mirror of
https://github.com/1f349/orchid.git
synced 2025-04-14 07:45:50 +01:00
Compare commits
31 Commits
Author | SHA1 | Date | |
---|---|---|---|
0d184e27e8 | |||
c737bab781 | |||
|
9e0ba09a84 | ||
|
4bbeaa87e1 | ||
|
35bc5ae4ab | ||
02bfb4f62f | |||
56be812ff5 | |||
8407f21090 | |||
fc2f8e34c6 | |||
a23155a827 | |||
432c907303 | |||
b642957aaf | |||
99c4c38bd5 | |||
c4c8c33139 | |||
939875ca4c | |||
7e70331179 | |||
645d22b856 | |||
c373f18336 | |||
c247a50472 | |||
4105d14e63 | |||
bb7c4bcedc | |||
0722b67969 | |||
619f909767 | |||
1f1db49160 | |||
|
f806c7d230 | ||
|
7768a339c8 | ||
7f24d37e31 | |||
d0fc76cd73 | |||
7e65015b89 | |||
37964ad546 | |||
76baa5f33f |
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@ -4,7 +4,7 @@ jobs:
|
||||
test:
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.x]
|
||||
go-version: [1.24.x]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/setup-go@v3
|
||||
|
233
agent/agent.go
Normal file
233
agent/agent.go
Normal file
@ -0,0 +1,233 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"github.com/1f349/orchid/database"
|
||||
"github.com/1f349/orchid/utils"
|
||||
"github.com/bramvdbogaerde/go-scp"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed agent_readme.md
|
||||
var agentReadme []byte
|
||||
|
||||
type agentQueries interface {
|
||||
FindAgentToSync(ctx context.Context) ([]database.FindAgentToSyncRow, error)
|
||||
UpdateAgentLastSync(ctx context.Context, row database.UpdateAgentLastSyncParams) error
|
||||
UpdateAgentCertNotAfter(ctx context.Context, arg database.UpdateAgentCertNotAfterParams) error
|
||||
}
|
||||
|
||||
func NewAgent(wg *sync.WaitGroup, db agentQueries, sshKey ssh.Signer, certDir string, keyDir string) (*Agent, error) {
|
||||
a := &Agent{
|
||||
db: db,
|
||||
ticker: time.NewTicker(time.Minute * 10),
|
||||
done: make(chan struct{}),
|
||||
syncLock: new(sync.Mutex),
|
||||
sshKey: sshKey,
|
||||
certDir: certDir,
|
||||
keyDir: keyDir,
|
||||
}
|
||||
|
||||
wg.Add(1)
|
||||
go a.syncRoutine(wg)
|
||||
return a, nil
|
||||
}
|
||||
|
||||
type Agent struct {
|
||||
db agentQueries
|
||||
ticker *time.Ticker
|
||||
done chan struct{}
|
||||
syncLock *sync.Mutex
|
||||
sshKey ssh.Signer
|
||||
certDir string
|
||||
keyDir string
|
||||
}
|
||||
|
||||
func (a *Agent) Shutdown() {
|
||||
Logger.Info("Shutting down agent syncing service")
|
||||
close(a.done)
|
||||
}
|
||||
|
||||
func (a *Agent) syncRoutine(wg *sync.WaitGroup) {
|
||||
Logger.Debug("Starting syncRoutine")
|
||||
|
||||
// Upon leaving the function stop the ticker and clear the WaitGroup.
|
||||
defer func() {
|
||||
a.ticker.Stop()
|
||||
Logger.Info("Stopped agent syncing service")
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
Logger.Info("Doing quick agent check before starting...")
|
||||
a.syncCheck()
|
||||
|
||||
// Logging or something
|
||||
Logger.Info("Initial check complete, continually checking every 10 minutes...")
|
||||
|
||||
// Main loop
|
||||
for {
|
||||
select {
|
||||
case <-a.done:
|
||||
// Exit if done has closed
|
||||
return
|
||||
case <-a.ticker.C:
|
||||
Logger.Debug("Ticking agent syncing")
|
||||
|
||||
go a.syncCheck()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) syncCheck() {
|
||||
// if the lock is unavailable then ignore this cycle
|
||||
if !a.syncLock.TryLock() {
|
||||
return
|
||||
}
|
||||
defer a.syncLock.Unlock()
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
actions, err := a.db.FindAgentToSync(context.Background())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
a.syncCertPairs(now, actions)
|
||||
}
|
||||
|
||||
type syncAgent struct {
|
||||
agentId int64
|
||||
address string
|
||||
user string
|
||||
fingerprint string
|
||||
}
|
||||
|
||||
func (a *Agent) syncCertPairs(startTime time.Time, rows []database.FindAgentToSyncRow) {
|
||||
agentMap := make(map[syncAgent][]database.FindAgentToSyncRow)
|
||||
|
||||
for _, row := range rows {
|
||||
a := syncAgent{
|
||||
agentId: row.AgentID,
|
||||
address: row.Address,
|
||||
user: row.User,
|
||||
fingerprint: row.Fingerprint,
|
||||
}
|
||||
agentMap[a] = append(agentMap[a], row)
|
||||
}
|
||||
|
||||
for agent, certPairs := range agentMap {
|
||||
err := a.syncSingleAgentCertPairs(startTime, agent, certPairs)
|
||||
if err != nil {
|
||||
// This agent sync is allowed to fail without stopping other agent syncs from
|
||||
// occurring.
|
||||
Logger.Warn("Agent sync failed", "agent", agent.agentId, "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Agent) syncSingleAgentCertPairs(startTime time.Time, agent syncAgent, rows []database.FindAgentToSyncRow) error {
|
||||
hostPubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(agent.fingerprint))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse fingerprint: %w", err)
|
||||
}
|
||||
|
||||
client, err := ssh.Dial("tcp", agent.address, &ssh.ClientConfig{
|
||||
Config: ssh.Config{
|
||||
KeyExchanges: []string{"curve25519-sha256"},
|
||||
},
|
||||
User: agent.user,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(a.sshKey),
|
||||
},
|
||||
HostKeyAlgorithms: []string{"ssh-ed25519"},
|
||||
HostKeyCallback: ssh.FixedHostKey(hostPubKey),
|
||||
Timeout: time.Second * 30,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("ssh dial: %w", err)
|
||||
}
|
||||
|
||||
scpClient, err := scp.NewClientBySSH(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("scp client: %w", err)
|
||||
}
|
||||
|
||||
hadError := false
|
||||
|
||||
for _, row := range rows {
|
||||
err := a.copySingleCertPair(&scpClient, row)
|
||||
if err != nil {
|
||||
// This cert sync is allowed to fail without stopping other certs going to the
|
||||
// same agent from copying.
|
||||
err = fmt.Errorf("copySingleCertPair: %w", err)
|
||||
Logger.Warn("Agent certificate sync failed", "agent", row.AgentID, "cert", row.CertID, "not after", row.CertNotAfter, "err", err)
|
||||
hadError = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// The agent last sync will only update if all scp copies were successful.
|
||||
if !hadError {
|
||||
// Update last sync to the time when the database request happened. This ensures
|
||||
// that certificates updated after the database request and before the agent
|
||||
// syncing are updated properly.
|
||||
err = a.db.UpdateAgentLastSync(context.Background(), database.UpdateAgentLastSyncParams{
|
||||
LastSync: sql.NullTime{Time: startTime, Valid: true},
|
||||
ID: agent.agentId,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating agent last sync: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Agent) copySingleCertPair(scpClient *scp.Client, row database.FindAgentToSyncRow) error {
|
||||
certName := utils.GetCertFileName(row.CertID)
|
||||
keyName := utils.GetKeyFileName(row.CertID)
|
||||
|
||||
certPath := filepath.Join(a.certDir, certName)
|
||||
keyPath := filepath.Join(a.keyDir, keyName)
|
||||
|
||||
// open cert and key files
|
||||
openCert, err := os.Open(certPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open cert file: %w", err)
|
||||
}
|
||||
defer openCert.Close()
|
||||
|
||||
openKey, err := os.Open(keyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open key file: %w", err)
|
||||
}
|
||||
defer openKey.Close()
|
||||
|
||||
// copy cert and key to agent
|
||||
err = scpClient.CopyFromFile(context.Background(), *openCert, filepath.Join(row.Dir, "certificates", certName), "0600")
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy cert file: %w", err)
|
||||
}
|
||||
err = scpClient.CopyFromFile(context.Background(), *openKey, filepath.Join(row.Dir, "keys", keyName), "0600")
|
||||
if err != nil {
|
||||
return fmt.Errorf("copy cert file: %w", err)
|
||||
}
|
||||
|
||||
err = a.db.UpdateAgentCertNotAfter(context.Background(), database.UpdateAgentCertNotAfterParams{
|
||||
NotAfter: row.CertNotAfter,
|
||||
AgentID: row.AgentID,
|
||||
CertID: row.CertID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error updating agent last sync: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
5
agent/agent_readme.md
Normal file
5
agent/agent_readme.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Orchid Agent
|
||||
|
||||
This directory is controlled by Orchid agent configuration.
|
||||
|
||||
Certificates in this directory will be automatically updated when required.
|
391
agent/agent_test.go
Normal file
391
agent/agent_test.go
Normal file
@ -0,0 +1,391 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509/pkix"
|
||||
"database/sql"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/1f349/orchid/database"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/mrmelon54/certgen"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestAgentSyncing(t *testing.T) {
|
||||
logger.Logger.SetLevel(log.DebugLevel)
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping agent syncing tests in short mode")
|
||||
}
|
||||
|
||||
t.Run("agent syncing test", func(t *testing.T) {
|
||||
certDir, err := os.MkdirTemp("", "orchid-certs")
|
||||
assert.NoError(t, err)
|
||||
keyDir, err := os.MkdirTemp("", "orchid-keys")
|
||||
assert.NoError(t, err)
|
||||
|
||||
defer func() {
|
||||
assert.NoError(t, os.RemoveAll(certDir))
|
||||
assert.NoError(t, os.RemoveAll(keyDir))
|
||||
}()
|
||||
|
||||
_, privKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sshPrivKey, err := ssh.NewSignerFromKey(privKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
agent := &Agent{
|
||||
db: &fakeAgentDb{},
|
||||
ticker: nil,
|
||||
done: nil,
|
||||
syncLock: nil,
|
||||
sshKey: sshPrivKey,
|
||||
certDir: certDir,
|
||||
keyDir: keyDir,
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
t.Run("missing cert file", func(t *testing.T) {
|
||||
err = agent.copySingleCertPair(nil, database.FindAgentToSyncRow{
|
||||
AgentID: 1337,
|
||||
Address: "",
|
||||
User: "test",
|
||||
Dir: "~/hello/world",
|
||||
Fingerprint: "",
|
||||
CertID: 420,
|
||||
CertNotAfter: sql.NullTime{Time: now, Valid: true},
|
||||
})
|
||||
assert.Contains(t, err.Error(), "open cert file:")
|
||||
assert.Contains(t, err.Error(), "no such file or directory")
|
||||
})
|
||||
|
||||
// generate example certificate
|
||||
tlsCert, err := certgen.MakeServerTls(nil, 2048, pkix.Name{
|
||||
Country: []string{"GB"},
|
||||
Province: []string{"London"},
|
||||
StreetAddress: []string{"221B Baker Street"},
|
||||
PostalCode: []string{"NW1 6XE"},
|
||||
SerialNumber: "test123456",
|
||||
CommonName: "orchid-agent-test.local",
|
||||
}, big.NewInt(1234567899), func(now time.Time) time.Time {
|
||||
return now.Add(1 * time.Hour)
|
||||
}, []string{"orchid-agent-test.local"}, []net.IP{
|
||||
net.IPv6loopback,
|
||||
net.IPv4(127, 0, 0, 1),
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = os.WriteFile(filepath.Join(certDir, "420.cert.pem"), tlsCert.GetCertPem(), 0600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("missing key file", func(t *testing.T) {
|
||||
err = agent.copySingleCertPair(nil, database.FindAgentToSyncRow{
|
||||
AgentID: 1337,
|
||||
Address: "",
|
||||
User: "test",
|
||||
Dir: "~/hello/world",
|
||||
Fingerprint: "",
|
||||
CertID: 420,
|
||||
CertNotAfter: sql.NullTime{Time: now, Valid: true},
|
||||
})
|
||||
assert.Contains(t, err.Error(), "open key file:")
|
||||
assert.Contains(t, err.Error(), "no such file or directory")
|
||||
})
|
||||
|
||||
err = os.WriteFile(filepath.Join(keyDir, "420.key.pem"), tlsCert.GetKeyPem(), 0600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
t.Run("successful sync", func(t *testing.T) {
|
||||
var wg sync.WaitGroup
|
||||
server := setupFakeSSH(&wg, func(remoteAddrPort netip.AddrPort, remotePubKey ssh.PublicKey) {
|
||||
println("Attempt agent syncing")
|
||||
|
||||
fingerprintStr := string(ssh.MarshalAuthorizedKey(remotePubKey))
|
||||
|
||||
err = agent.syncSingleAgentCertPairs(now, syncAgent{
|
||||
agentId: 1337,
|
||||
address: remoteAddrPort.String(),
|
||||
user: "test",
|
||||
fingerprint: fingerprintStr,
|
||||
}, []database.FindAgentToSyncRow{
|
||||
{
|
||||
AgentID: 1337,
|
||||
Address: remoteAddrPort.String(),
|
||||
User: "test",
|
||||
Dir: "~/hello/world",
|
||||
Fingerprint: fingerprintStr,
|
||||
CertID: 420,
|
||||
CertNotAfter: sql.NullTime{Time: now, Valid: true},
|
||||
},
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
server.Close()
|
||||
|
||||
println("Waiting for ssh server to exit")
|
||||
|
||||
server.Wait()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func setupFakeSSH(wg *sync.WaitGroup, call func(addrPort netip.AddrPort, pubKey ssh.PublicKey)) *ssh.ServerConn {
|
||||
pubKey, privKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sshPubKey, err := ssh.NewPublicKey(pubKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sshSigner, err := ssh.NewSignerFromKey(privKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tcp, err := net.ListenTCP("tcp", net.TCPAddrFromAddrPort(netip.AddrPortFrom(netip.IPv6Loopback(), 0)))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
addrPort := tcp.Addr().(*net.TCPAddr).AddrPort()
|
||||
|
||||
var wg2 sync.WaitGroup
|
||||
wg2.Add(1)
|
||||
go func() {
|
||||
defer wg2.Done()
|
||||
call(addrPort, sshPubKey)
|
||||
}()
|
||||
|
||||
tcpConn, err := tcp.AcceptTCP()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
serverConfig := &ssh.ServerConfig{
|
||||
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||
if conn.User() != "test" {
|
||||
return nil, fmt.Errorf("invalid user")
|
||||
}
|
||||
if !conn.RemoteAddr().(*net.TCPAddr).AddrPort().Addr().IsLoopback() {
|
||||
return nil, fmt.Errorf("invalid remote address")
|
||||
}
|
||||
return &ssh.Permissions{}, nil
|
||||
},
|
||||
ServerVersion: "SSH-2.0-OrchidTester",
|
||||
}
|
||||
serverConfig.AddHostKey(sshSigner)
|
||||
|
||||
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, serverConfig)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// The incoming Request channel must be serviced.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
ssh.DiscardRequests(reqs)
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
// Service the incoming channel.
|
||||
for newChannel := range chans {
|
||||
// Channels have a type, depending on the application level
|
||||
// protocol intended. In the case of a shell, the type is
|
||||
// "session" and ServerShell may be used to present a simple
|
||||
// terminal interface.
|
||||
if newChannel.ChannelType() != "session" {
|
||||
newChannel.Reject(ssh.UnknownChannelType, "unknown channel type")
|
||||
continue
|
||||
}
|
||||
channel, requests, err := newChannel.Accept()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var fullFilePath *string
|
||||
{
|
||||
a := ""
|
||||
fullFilePath = &a
|
||||
}
|
||||
|
||||
// Sessions have out-of-band requests such as "shell",
|
||||
// "pty-req" and "env". Here we handle only the
|
||||
// "shell" request.
|
||||
wg.Add(1)
|
||||
go func(in <-chan *ssh.Request) {
|
||||
for req := range in {
|
||||
req.Reply(req.Type == "exec", nil)
|
||||
if req.Type == "exec" {
|
||||
length := binary.BigEndian.Uint32(req.Payload[:4])
|
||||
if len(req.Payload) != int(length)+4 {
|
||||
panic(fmt.Errorf("invalid exec payload (expected %d but got %d)", length, len(req.Payload)))
|
||||
}
|
||||
cmd := string(req.Payload[4:])
|
||||
const scpStartStr = "scp -qt \""
|
||||
if !strings.HasPrefix(cmd, scpStartStr) {
|
||||
panic("invalid start")
|
||||
}
|
||||
if !strings.HasSuffix(cmd, "\"") {
|
||||
panic("invalid end")
|
||||
}
|
||||
filePath := cmd[len(scpStartStr) : len(cmd)-1]
|
||||
fmt.Println("Writing file:", filePath)
|
||||
*fullFilePath = filePath
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(requests)
|
||||
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer func() {
|
||||
channel.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
||||
var b [1024]byte
|
||||
read := must(channel.Read(b[:]))
|
||||
if read < 1 {
|
||||
panic("invalid read")
|
||||
}
|
||||
|
||||
fmt.Println(string(b[:read]))
|
||||
|
||||
r := bufio.NewReader(bytes.NewReader(b[:read]))
|
||||
if readByte(r) != 'C' {
|
||||
panic("invalid scp command")
|
||||
}
|
||||
|
||||
fileMode := readN(r, 4)
|
||||
if string(fileMode) != "0600" {
|
||||
panic("unexpected file mode")
|
||||
}
|
||||
|
||||
if readByte(r) != ' ' {
|
||||
panic("missing space")
|
||||
}
|
||||
|
||||
fileSizeStr := must(r.ReadString(' '))
|
||||
fileSize := must(strconv.Atoi(fileSizeStr[:len(fileSizeStr)-1]))
|
||||
|
||||
fileName := strings.TrimSpace(string(must(io.ReadAll(r))))
|
||||
if fileName != filepath.Base(*fullFilePath) {
|
||||
panic(fmt.Errorf("invalid file name (expected \"%s\" from full path \"%s\" but got \"%s\")", filepath.Base(*fullFilePath), *fullFilePath, fileName))
|
||||
}
|
||||
|
||||
if fileName != "420.cert.pem" && fileName != "420.key.pem" {
|
||||
panic("invalid file name")
|
||||
}
|
||||
|
||||
channel.Write([]byte{0})
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := io.CopyN(buf, channel, int64(fileSize))
|
||||
if err != nil {
|
||||
panic("Failed to copy channel")
|
||||
}
|
||||
fmt.Println("Copied file with size:", buf.Len())
|
||||
fmt.Println(buf.String())
|
||||
|
||||
if readLastByte(r) != 0x00 {
|
||||
panic("expected ending null byte")
|
||||
}
|
||||
|
||||
channel.Write([]byte{0})
|
||||
|
||||
channel.SendRequest("exit-status", false, binary.BigEndian.AppendUint32(nil, 0))
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
wg2.Wait()
|
||||
|
||||
return sshConn
|
||||
}
|
||||
|
||||
type fakeAgentDb struct{}
|
||||
|
||||
func (f *fakeAgentDb) FindAgentToSync(ctx context.Context) ([]database.FindAgentToSyncRow, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (f *fakeAgentDb) UpdateAgentLastSync(ctx context.Context, arg database.UpdateAgentLastSyncParams) error {
|
||||
if arg.ID != 1337 {
|
||||
return fmt.Errorf("invalid agent id")
|
||||
}
|
||||
if !arg.LastSync.Valid {
|
||||
return fmt.Errorf("invalid last sync")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeAgentDb) UpdateAgentCertNotAfter(ctx context.Context, arg database.UpdateAgentCertNotAfterParams) error {
|
||||
if arg.AgentID != 1337 {
|
||||
return fmt.Errorf("invalid agent id")
|
||||
}
|
||||
if arg.CertID != 420 {
|
||||
return fmt.Errorf("invalid cert id")
|
||||
}
|
||||
if !arg.NotAfter.Valid {
|
||||
return fmt.Errorf("invalid not after")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func must[T any](t T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func readN(r io.Reader, n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, err := io.ReadFull(r, b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func readByte(r io.Reader) byte {
|
||||
b := readN(r, 1)
|
||||
return b[0]
|
||||
}
|
||||
|
||||
func readLastByte(r io.Reader) byte {
|
||||
var b [1]byte
|
||||
_, err := io.ReadFull(r, b[:])
|
||||
if !errors.Is(err, io.EOF) {
|
||||
panic("expected EOF")
|
||||
}
|
||||
return b[0]
|
||||
}
|
5
agent/logger.go
Normal file
5
agent/logger.go
Normal file
@ -0,0 +1,5 @@
|
||||
package agent
|
||||
|
||||
import "github.com/1f349/orchid/logger"
|
||||
|
||||
var Logger = logger.Logger.WithPrefix("Orchid Agent")
|
65
cmd/orchid/agent.go
Normal file
65
cmd/orchid/agent.go
Normal file
@ -0,0 +1,65 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/pem"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// loadAgentPrivateKey simply attempts to load the agent ssh private key and if
|
||||
// it is missing generates a new key
|
||||
func loadAgentPrivateKey(wd string) ssh.Signer {
|
||||
// load or create a key for orchid agent
|
||||
agentPrivKeyPath := filepath.Join(wd, "agent_id_ed25519")
|
||||
agentPubKeyPath := filepath.Join(wd, "agent_id_ed25519.pub")
|
||||
agentPrivKeyBytes, err := os.ReadFile(agentPrivKeyPath)
|
||||
switch {
|
||||
case err == nil:
|
||||
break
|
||||
case os.IsNotExist(err):
|
||||
pubKey, privKey, err := ed25519.GenerateKey(nil)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to generate agent private key", "err", err)
|
||||
}
|
||||
marshalPrivKey, err := ssh.MarshalPrivateKey(privKey, "orchid-agent")
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to encode private key", "err", err)
|
||||
}
|
||||
agentPrivKeyBytes = pem.EncodeToMemory(marshalPrivKey)
|
||||
|
||||
// public key
|
||||
sshPubKey, err := ssh.NewPublicKey(pubKey)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to encode public key", "err", err)
|
||||
}
|
||||
marshalPubKey := ssh.MarshalAuthorizedKey(sshPubKey)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to encode public key", "err", err)
|
||||
}
|
||||
|
||||
// write to files
|
||||
err = os.WriteFile(agentPrivKeyPath, agentPrivKeyBytes, 0600)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to write agent private key", "path", agentPrivKeyPath, "err", err)
|
||||
}
|
||||
err = os.WriteFile(agentPubKeyPath, marshalPubKey, 0644)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to write agent public key", "path", agentPubKeyPath, "err", err)
|
||||
}
|
||||
|
||||
// we can continue now
|
||||
break
|
||||
case err != nil:
|
||||
logger.Logger.Fatal("Failed to read agent private key", "path", agentPrivKeyPath, "err", err)
|
||||
}
|
||||
|
||||
privKey, err := ssh.ParsePrivateKey(agentPrivKeyBytes)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to parse agent private key file", "path", agentPrivKeyPath, "err", err)
|
||||
}
|
||||
|
||||
return privKey
|
||||
}
|
@ -3,10 +3,11 @@ package main
|
||||
import "github.com/1f349/orchid/renewal"
|
||||
|
||||
type startUpConfig struct {
|
||||
Listen string `yaml:"listen"`
|
||||
Acme acmeConfig `yaml:"acme"`
|
||||
LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
|
||||
Domains []string `yaml:"domains"`
|
||||
Listen string `yaml:"listen"`
|
||||
Acme acmeConfig `yaml:"acme"`
|
||||
LE renewal.LetsEncryptConfig `yaml:"letsEncrypt"`
|
||||
Domains []string `yaml:"domains"`
|
||||
AgentKey string `yaml:"agentKey"`
|
||||
}
|
||||
|
||||
type acmeConfig struct {
|
||||
|
@ -1,20 +1,116 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"github.com/google/subcommands"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/orchid"
|
||||
"github.com/1f349/orchid/agent"
|
||||
httpAcme "github.com/1f349/orchid/http-acme"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"github.com/1f349/orchid/renewal"
|
||||
"github.com/1f349/orchid/servers"
|
||||
"github.com/1f349/violet/utils"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
exitReload "github.com/mrmelon54/exit-reload"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func main() {
|
||||
subcommands.Register(subcommands.HelpCommand(), "")
|
||||
subcommands.Register(subcommands.FlagsCommand(), "")
|
||||
subcommands.Register(subcommands.CommandsCommand(), "")
|
||||
subcommands.Register(&serveCmd{}, "")
|
||||
subcommands.Register(&setupCmd{}, "")
|
||||
var configPath string
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&configPath, "conf", "", "/path/to/config.json : path to the config file")
|
||||
flag.Parse()
|
||||
ctx := context.Background()
|
||||
os.Exit(int(subcommands.Execute(ctx)))
|
||||
|
||||
logger.Logger.Info("Starting...")
|
||||
|
||||
if configPath == "" {
|
||||
logger.Logger.Error("Config flag is missing")
|
||||
trySetup(configPath)
|
||||
return
|
||||
}
|
||||
|
||||
wd, err := getWD(configPath)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to find config directory", "err", err)
|
||||
}
|
||||
|
||||
// try to open the config file
|
||||
openConf, err := os.Open(configPath)
|
||||
switch {
|
||||
case err == nil:
|
||||
break
|
||||
case os.IsNotExist(err):
|
||||
logger.Logger.Warn("Failed to open config file", "err", err)
|
||||
trySetup(wd)
|
||||
return
|
||||
default:
|
||||
logger.Logger.Fatal("Open config file", "err", err)
|
||||
}
|
||||
|
||||
// config file opened with no errors
|
||||
|
||||
defer openConf.Close()
|
||||
|
||||
var config startUpConfig
|
||||
err = yaml.NewDecoder(openConf).Decode(&config)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Invalid config file", "err", err)
|
||||
}
|
||||
|
||||
runDaemon(wd, config)
|
||||
}
|
||||
|
||||
func runDaemon(wd string, conf startUpConfig) {
|
||||
// load the MJWT RSA public key from a pem encoded file
|
||||
mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys"))
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to load MJWT verifier public key from file", "path", filepath.Join(wd, "keys"), "err", err)
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite"))
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to open database", "err", err)
|
||||
}
|
||||
|
||||
certDir := filepath.Join(wd, "renewal-certs")
|
||||
keyDir := filepath.Join(wd, "renewal-keys")
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
acmeProv, err := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.yml"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("HTTP Acme Error", "err", err)
|
||||
}
|
||||
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Service Error", "err", err)
|
||||
}
|
||||
certAgent, err := agent.NewAgent(wg, db, loadAgentPrivateKey(wd), certDir, keyDir)
|
||||
if err != nil {
|
||||
logger.Logger.Fatal("Failed to create agent", "err", err)
|
||||
}
|
||||
srv := servers.NewApiServer(conf.Listen, db, mJwtVerify, conf.Domains)
|
||||
logger.Logger.Info("Starting API server", "listen", srv.Addr)
|
||||
go utils.RunBackgroundHttp(logger.Logger, srv)
|
||||
|
||||
exitReload.ExitReload("Violet", func() {}, func() {
|
||||
// stop renewal service and api server
|
||||
renewalService.Shutdown()
|
||||
certAgent.Shutdown()
|
||||
srv.Close()
|
||||
})
|
||||
}
|
||||
|
||||
func getWD(configPath string) (string, error) {
|
||||
if configPath == "" {
|
||||
return os.Getwd()
|
||||
}
|
||||
wdAbs, err := filepath.Abs(configPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Dir(wdAbs), nil
|
||||
}
|
||||
|
@ -1,100 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"github.com/1f349/mjwt"
|
||||
"github.com/1f349/orchid"
|
||||
httpAcme "github.com/1f349/orchid/http-acme"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"github.com/1f349/orchid/renewal"
|
||||
"github.com/1f349/orchid/servers"
|
||||
"github.com/1f349/violet/utils"
|
||||
"github.com/google/subcommands"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/mrmelon54/exit-reload"
|
||||
"gopkg.in/yaml.v3"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type serveCmd struct{ configPath string }
|
||||
|
||||
func (s *serveCmd) Name() string { return "serve" }
|
||||
func (s *serveCmd) Synopsis() string { return "Serve certificate renewal service" }
|
||||
func (s *serveCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&s.configPath, "conf", "", "/path/to/config.json : path to the config file")
|
||||
}
|
||||
func (s *serveCmd) Usage() string {
|
||||
return `serve [-conf <config file>]
|
||||
Serve certificate renewal service using information from config file
|
||||
`
|
||||
}
|
||||
|
||||
func (s *serveCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
|
||||
logger.Logger.Info("[Orchid] Starting...")
|
||||
|
||||
if s.configPath == "" {
|
||||
logger.Logger.Info("[Orchid] Error: config flag is missing")
|
||||
return subcommands.ExitUsageError
|
||||
}
|
||||
|
||||
openConf, err := os.Open(s.configPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logger.Logger.Info("[Orchid] Error: missing config file")
|
||||
} else {
|
||||
logger.Logger.Info("[Orchid] Error: open config file: ", "err", err)
|
||||
}
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
var conf startUpConfig
|
||||
err = yaml.NewDecoder(openConf).Decode(&conf)
|
||||
if err != nil {
|
||||
logger.Logger.Info("[Orchid] Error: invalid config file: ", "err", err)
|
||||
return subcommands.ExitFailure
|
||||
}
|
||||
|
||||
wd := filepath.Dir(s.configPath)
|
||||
normalLoad(conf, wd)
|
||||
return subcommands.ExitSuccess
|
||||
}
|
||||
|
||||
func normalLoad(conf startUpConfig, wd string) {
|
||||
// load the MJWT RSA public key from a pem encoded file
|
||||
mJwtVerify, err := mjwt.NewKeyStoreFromPath(filepath.Join(wd, "keys"))
|
||||
if err != nil {
|
||||
log.Fatalf("[Orchid] Failed to load MJWT verifier public key from file '%s': %s", filepath.Join(wd, "keys"), err)
|
||||
}
|
||||
|
||||
// open sqlite database
|
||||
db, err := orchid.InitDB(filepath.Join(wd, "orchid.db.sqlite"))
|
||||
if err != nil {
|
||||
log.Fatal("[Orchid] Failed to open database:", err)
|
||||
}
|
||||
|
||||
certDir := filepath.Join(wd, "certs")
|
||||
keyDir := filepath.Join(wd, "keys")
|
||||
|
||||
wg := &sync.WaitGroup{}
|
||||
acmeProv, err := httpAcme.NewHttpAcmeProvider(filepath.Join(wd, "tokens.yml"), conf.Acme.PresentUrl, conf.Acme.CleanUpUrl, conf.Acme.RefreshUrl)
|
||||
if err != nil {
|
||||
log.Fatal("[Orchid] HTTP Acme Error:", err)
|
||||
}
|
||||
renewalService, err := renewal.NewService(wg, db, acmeProv, conf.LE, certDir, keyDir)
|
||||
if err != nil {
|
||||
log.Fatal("[Orchid] Service Error:", err)
|
||||
}
|
||||
srv := servers.NewApiServer(conf.Listen, db, mJwtVerify, conf.Domains)
|
||||
logger.Logger.Info("Starting API server", "listen", srv.Addr)
|
||||
go utils.RunBackgroundHttp(logger.Logger, srv)
|
||||
|
||||
exit_reload.ExitReload("Violet", func() {}, func() {
|
||||
// stop renewal service and api server
|
||||
renewalService.Shutdown()
|
||||
srv.Close()
|
||||
})
|
||||
}
|
@ -2,16 +2,15 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"errors"
|
||||
"fmt"
|
||||
httpAcme "github.com/1f349/orchid/http-acme"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"github.com/1f349/orchid/renewal"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/google/subcommands"
|
||||
"gopkg.in/yaml.v3"
|
||||
"math/rand"
|
||||
"net"
|
||||
@ -22,37 +21,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type setupCmd struct{ wdPath string }
|
||||
var errExitSetup = errors.New("exit setup")
|
||||
|
||||
func (s *setupCmd) Name() string { return "setup" }
|
||||
func (s *setupCmd) Synopsis() string { return "Setup certificate renewal service" }
|
||||
func (s *setupCmd) SetFlags(f *flag.FlagSet) {
|
||||
f.StringVar(&s.wdPath, "wd", ".", "Path to the directory to create config files in (defaults to the working directory)")
|
||||
}
|
||||
func (s *setupCmd) Usage() string {
|
||||
return `setup [-wd <directory>]
|
||||
Setup Orchid automatically by answering questions.
|
||||
`
|
||||
}
|
||||
|
||||
func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
|
||||
// get absolute path to specify files
|
||||
wdAbs, err := filepath.Abs(s.wdPath)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Failed to get full directory path: ", err)
|
||||
return subcommands.ExitFailure
|
||||
func trySetup(wd string) {
|
||||
// handle potential errors during setup
|
||||
err := runSetup(wd)
|
||||
switch {
|
||||
case errors.Is(err, errExitSetup):
|
||||
// exit setup without questions
|
||||
return
|
||||
case err == nil:
|
||||
return
|
||||
default:
|
||||
logger.Logger.Fatal("Failed to run setup", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func runSetup(wd string) error {
|
||||
// ask about running the setup steps
|
||||
createFile := false
|
||||
err = survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wdAbs)}, &createFile)
|
||||
err := survey.AskOne(&survey.Confirm{Message: fmt.Sprintf("Create Orchid config files in this directory: '%s'?", wd)}, &createFile)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return err
|
||||
}
|
||||
if !createFile {
|
||||
fmt.Println("[Orchid] Goodbye")
|
||||
return subcommands.ExitSuccess
|
||||
logger.Logger.Info("Goodbye")
|
||||
return errExitSetup
|
||||
}
|
||||
|
||||
var answers struct {
|
||||
@ -64,7 +58,6 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
||||
AcmeRefreshUrl string
|
||||
LEEmail string
|
||||
}
|
||||
_ = answers
|
||||
|
||||
// ask main questions
|
||||
err = survey.Ask([]*survey.Question{
|
||||
@ -88,8 +81,7 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
||||
},
|
||||
}, &answers)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return err
|
||||
}
|
||||
|
||||
if answers.AcmeRefresh != "" {
|
||||
@ -111,35 +103,31 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
||||
},
|
||||
}, &answers)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
key, err := rsa.GenerateKey(rand.New(rand.NewSource(time.Now().UnixNano())), 4096)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return fmt.Errorf("failed to generate private key: %w", err)
|
||||
}
|
||||
keyBytes := x509.MarshalPKCS1PrivateKey(key)
|
||||
keyBuf := new(bytes.Buffer)
|
||||
err = pem.Encode(keyBuf, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: keyBytes})
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Error: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return fmt.Errorf("failed to PEM encode private key: %w", err)
|
||||
}
|
||||
|
||||
// write config file
|
||||
confFile := filepath.Join(wdAbs, "config.yml")
|
||||
confFile := filepath.Join(wd, "config.yml")
|
||||
createConf, err := os.Create(confFile)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Failed to create config file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return fmt.Errorf("failed to create config file: %w", err)
|
||||
}
|
||||
defer createConf.Close()
|
||||
|
||||
confEncode := yaml.NewEncoder(createConf)
|
||||
confEncode.SetIndent(2)
|
||||
err = confEncode.Encode(startUpConfig{
|
||||
// this is the whole config structure
|
||||
config := startUpConfig{
|
||||
Listen: answers.ApiListen,
|
||||
Acme: acmeConfig{
|
||||
PresentUrl: answers.AcmePresentUrl,
|
||||
@ -155,18 +143,20 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
||||
Certificate: "default",
|
||||
},
|
||||
Domains: strings.Split(answers.ApiDomains, ","),
|
||||
})
|
||||
}
|
||||
|
||||
confEncode := yaml.NewEncoder(createConf)
|
||||
confEncode.SetIndent(2)
|
||||
err = confEncode.Encode(config)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Failed to write config file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
// write token file
|
||||
tokenFile := filepath.Join(wdAbs, "tokens.yml")
|
||||
tokenFile := filepath.Join(wd, "tokens.yml")
|
||||
createTokens, err := os.Create(tokenFile)
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Failed to create tokens file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return fmt.Errorf("failed to create tokens file: %w", err)
|
||||
}
|
||||
|
||||
confEncode = yaml.NewEncoder(createTokens)
|
||||
@ -176,14 +166,13 @@ func (s *setupCmd) Execute(ctx context.Context, f *flag.FlagSet, args ...interfa
|
||||
Refresh: answers.AcmeRefresh,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Println("[Orchid] Failed to write tokens file: ", err)
|
||||
return subcommands.ExitFailure
|
||||
return fmt.Errorf("failed to write tokens file: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("[Orchid] Setup complete")
|
||||
fmt.Printf("[Orchid] Run the renewal service with `orchid serve -conf %s`\n", confFile)
|
||||
logger.Logger.Info("Setup complete")
|
||||
logger.Logger.Infof("Run the renewal service with `orchid-daemon -conf %s`", confFile)
|
||||
|
||||
return subcommands.ExitSuccess
|
||||
return nil
|
||||
}
|
||||
|
||||
func listenAddressValidator(ans interface{}) error {
|
||||
|
98
database/agent.sql.go
Normal file
98
database/agent.sql.go
Normal file
@ -0,0 +1,98 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.28.0
|
||||
// source: agent.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
const findAgentToSync = `-- name: FindAgentToSync :many
|
||||
SELECT agents.id as agent_id, agents.address, agents.user, agents.dir, agents.fingerprint, cert.id as cert_id, cert.not_after as cert_not_after
|
||||
FROM agents
|
||||
INNER JOIN agent_certs
|
||||
ON agent_certs.agent_id = agents.id
|
||||
INNER JOIN certificates AS cert
|
||||
ON cert.id = agent_certs.cert_id
|
||||
WHERE (agents.last_sync IS NULL OR agents.last_sync < cert.updated_at)
|
||||
AND (agent_certs.not_after IS NULL OR agent_certs.not_after IS NOT cert.not_after)
|
||||
ORDER BY agents.last_sync NULLS FIRST
|
||||
`
|
||||
|
||||
type FindAgentToSyncRow struct {
|
||||
AgentID int64 `json:"agent_id"`
|
||||
Address string `json:"address"`
|
||||
User string `json:"user"`
|
||||
Dir string `json:"dir"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
CertID int64 `json:"cert_id"`
|
||||
CertNotAfter sql.NullTime `json:"cert_not_after"`
|
||||
}
|
||||
|
||||
func (q *Queries) FindAgentToSync(ctx context.Context) ([]FindAgentToSyncRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, findAgentToSync)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []FindAgentToSyncRow
|
||||
for rows.Next() {
|
||||
var i FindAgentToSyncRow
|
||||
if err := rows.Scan(
|
||||
&i.AgentID,
|
||||
&i.Address,
|
||||
&i.User,
|
||||
&i.Dir,
|
||||
&i.Fingerprint,
|
||||
&i.CertID,
|
||||
&i.CertNotAfter,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateAgentCertNotAfter = `-- name: UpdateAgentCertNotAfter :exec
|
||||
UPDATE agent_certs
|
||||
SET not_after = ?
|
||||
WHERE agent_id = ?
|
||||
AND cert_id = ?
|
||||
`
|
||||
|
||||
type UpdateAgentCertNotAfterParams struct {
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
AgentID int64 `json:"agent_id"`
|
||||
CertID int64 `json:"cert_id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAgentCertNotAfter(ctx context.Context, arg UpdateAgentCertNotAfterParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAgentCertNotAfter, arg.NotAfter, arg.AgentID, arg.CertID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateAgentLastSync = `-- name: UpdateAgentLastSync :exec
|
||||
UPDATE agents
|
||||
SET last_sync = ?
|
||||
WHERE agents.id = ?
|
||||
`
|
||||
|
||||
type UpdateAgentLastSyncParams struct {
|
||||
LastSync sql.NullTime `json:"last_sync"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAgentLastSync(ctx context.Context, arg UpdateAgentLastSyncParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateAgentLastSync, arg.LastSync, arg.ID)
|
||||
return err
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: certificate.sql
|
||||
|
||||
package database
|
||||
@ -19,7 +19,7 @@ VALUES (?, ?, ?, ?)
|
||||
type AddCertificateParams struct {
|
||||
Owner string `json:"owner"`
|
||||
Dns sql.NullInt64 `json:"dns"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ FROM certificates AS cert
|
||||
WHERE cert.active = 1
|
||||
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
|
||||
AND cert.renewing = 0
|
||||
AND cert.renew_failed = 0
|
||||
AND (cert.renew_retry IS NULL OR DATETIME() > DATETIME(cert.renew_retry))
|
||||
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
|
||||
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
|
||||
LIMIT 1
|
||||
@ -83,7 +83,7 @@ LIMIT 1
|
||||
|
||||
type FindNextCertRow struct {
|
||||
ID int64 `json:"id"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
Type sql.NullString `json:"type"`
|
||||
Token sql.NullString `json:"token"`
|
||||
TempParent sql.NullInt64 `json:"temp_parent"`
|
||||
@ -107,7 +107,7 @@ SELECT cert.id,
|
||||
cert.auto_renew,
|
||||
cert.active,
|
||||
cert.renewing,
|
||||
cert.renew_failed,
|
||||
cert.renew_retry,
|
||||
cert.not_after,
|
||||
cert.updated_at,
|
||||
certificate_domains.domain
|
||||
@ -116,14 +116,14 @@ FROM certificates AS cert
|
||||
`
|
||||
|
||||
type FindOwnedCertsRow struct {
|
||||
ID int64 `json:"id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Domain string `json:"domain"`
|
||||
ID int64 `json:"id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewRetry sql.NullTime `json:"renew_retry"`
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, error) {
|
||||
@ -140,7 +140,7 @@ func (q *Queries) FindOwnedCerts(ctx context.Context) ([]FindOwnedCertsRow, erro
|
||||
&i.AutoRenew,
|
||||
&i.Active,
|
||||
&i.Renewing,
|
||||
&i.RenewFailed,
|
||||
&i.RenewRetry,
|
||||
&i.NotAfter,
|
||||
&i.UpdatedAt,
|
||||
&i.Domain,
|
||||
@ -169,19 +169,30 @@ func (q *Queries) RemoveCertificate(ctx context.Context, id int64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
const setRetryFlag = `-- name: SetRetryFlag :exec
|
||||
UPDATE certificates
|
||||
SET renew_retry = DATETIME('now', '+1 day')
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) SetRetryFlag(ctx context.Context, id int64) error {
|
||||
_, err := q.db.ExecContext(ctx, setRetryFlag, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateCertAfterRenewal = `-- name: UpdateCertAfterRenewal :exec
|
||||
UPDATE certificates
|
||||
SET renewing = 0,
|
||||
renew_failed=0,
|
||||
SET renewing = 0,
|
||||
renew_retry=0,
|
||||
not_after=?,
|
||||
updated_at=?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateCertAfterRenewalParams struct {
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfterRenewalParams) error {
|
||||
@ -191,18 +202,18 @@ func (q *Queries) UpdateCertAfterRenewal(ctx context.Context, arg UpdateCertAfte
|
||||
|
||||
const updateRenewingState = `-- name: UpdateRenewingState :exec
|
||||
UPDATE certificates
|
||||
SET renewing = ?,
|
||||
renew_failed = ?
|
||||
SET renewing = ?,
|
||||
renew_retry = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
type UpdateRenewingStateParams struct {
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
ID int64 `json:"id"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewRetry sql.NullTime `json:"renew_retry"`
|
||||
ID int64 `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateRenewingState(ctx context.Context, arg UpdateRenewingStateParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewFailed, arg.ID)
|
||||
_, err := q.db.ExecContext(ctx, updateRenewingState, arg.Renewing, arg.RenewRetry, arg.ID)
|
||||
return err
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
// source: certificate_domains.sql
|
||||
|
||||
package database
|
||||
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package database
|
||||
|
||||
|
5
database/migrations/20240920175046_retry_renewal.up.sql
Normal file
5
database/migrations/20240920175046_retry_renewal.up.sql
Normal file
@ -0,0 +1,5 @@
|
||||
ALTER TABLE certificates
|
||||
DROP COLUMN renew_failed;
|
||||
|
||||
ALTER TABLE certificates
|
||||
ADD COLUMN renew_retry DATETIME NOT NULL DEFAULT 0;
|
0
database/migrations/20250129211955_agent.down.sql
Normal file
0
database/migrations/20250129211955_agent.down.sql
Normal file
21
database/migrations/20250129211955_agent.up.sql
Normal file
21
database/migrations/20250129211955_agent.up.sql
Normal file
@ -0,0 +1,21 @@
|
||||
CREATE TABLE IF NOT EXISTS agents
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
address TEXT NOT NULL,
|
||||
user TEXT NOT NULL,
|
||||
dir TEXT NOT NULL,
|
||||
fingerprint TEXT NOT NULL,
|
||||
last_sync DATETIME NULL DEFAULT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS agent_certs
|
||||
(
|
||||
agent_id INTEGER NOT NULL,
|
||||
cert_id INTEGER NOT NULL,
|
||||
not_after INTEGER NULL DEFAULT NULL,
|
||||
|
||||
PRIMARY KEY (agent_id, cert_id),
|
||||
|
||||
FOREIGN KEY (agent_id) REFERENCES agents (id),
|
||||
FOREIGN KEY (cert_id) REFERENCES certificates (id)
|
||||
);
|
29
database/migrations/20250131183447_null_not_after.up.sql
Normal file
29
database/migrations/20250131183447_null_not_after.up.sql
Normal file
@ -0,0 +1,29 @@
|
||||
-- null not after
|
||||
|
||||
ALTER TABLE certificates
|
||||
RENAME COLUMN not_after TO not_after_2;
|
||||
|
||||
ALTER TABLE certificates
|
||||
ADD COLUMN not_after DATETIME NULL;
|
||||
|
||||
UPDATE certificates
|
||||
SET not_after = not_after_2
|
||||
WHERE not_after IS NULL;
|
||||
|
||||
ALTER TABLE certificates
|
||||
DROP COLUMN not_after_2;
|
||||
|
||||
-- null renew retry
|
||||
|
||||
ALTER TABLE certificates
|
||||
RENAME COLUMN renew_retry TO renew_retry_2;
|
||||
|
||||
ALTER TABLE certificates
|
||||
ADD COLUMN renew_retry DATETIME NULL;
|
||||
|
||||
UPDATE certificates
|
||||
SET renew_retry = renew_retry_2
|
||||
WHERE renew_retry IS NULL;
|
||||
|
||||
ALTER TABLE certificates
|
||||
DROP COLUMN renew_retry_2;
|
@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.25.0
|
||||
// sqlc v1.28.0
|
||||
|
||||
package database
|
||||
|
||||
@ -9,17 +9,32 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type Agent struct {
|
||||
ID int64 `json:"id"`
|
||||
Address string `json:"address"`
|
||||
User string `json:"user"`
|
||||
Dir string `json:"dir"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
LastSync sql.NullTime `json:"last_sync"`
|
||||
}
|
||||
|
||||
type AgentCert struct {
|
||||
AgentID int64 `json:"agent_id"`
|
||||
CertID int64 `json:"cert_id"`
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
}
|
||||
|
||||
type Certificate struct {
|
||||
ID int64 `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Dns sql.NullInt64 `json:"dns"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
TempParent sql.NullInt64 `json:"temp_parent"`
|
||||
ID int64 `json:"id"`
|
||||
Owner string `json:"owner"`
|
||||
Dns sql.NullInt64 `json:"dns"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
TempParent sql.NullInt64 `json:"temp_parent"`
|
||||
NotAfter sql.NullTime `json:"not_after"`
|
||||
RenewRetry sql.NullTime `json:"renew_retry"`
|
||||
}
|
||||
|
||||
type CertificateDomain struct {
|
||||
|
21
database/queries/agent.sql
Normal file
21
database/queries/agent.sql
Normal file
@ -0,0 +1,21 @@
|
||||
-- name: FindAgentToSync :many
|
||||
SELECT agents.id as agent_id, agents.address, agents.user, agents.dir, agents.fingerprint, cert.id as cert_id, cert.not_after as cert_not_after
|
||||
FROM agents
|
||||
INNER JOIN agent_certs
|
||||
ON agent_certs.agent_id = agents.id
|
||||
INNER JOIN certificates AS cert
|
||||
ON cert.id = agent_certs.cert_id
|
||||
WHERE (agents.last_sync IS NULL OR agents.last_sync < cert.updated_at)
|
||||
AND (agent_certs.not_after IS NULL OR agent_certs.not_after IS NOT cert.not_after)
|
||||
ORDER BY agents.last_sync NULLS FIRST;
|
||||
|
||||
-- name: UpdateAgentLastSync :exec
|
||||
UPDATE agents
|
||||
SET last_sync = ?
|
||||
WHERE agents.id = ?;
|
||||
|
||||
-- name: UpdateAgentCertNotAfter :exec
|
||||
UPDATE agent_certs
|
||||
SET not_after = ?
|
||||
WHERE agent_id = ?
|
||||
AND cert_id = ?;
|
@ -5,7 +5,7 @@ FROM certificates AS cert
|
||||
WHERE cert.active = 1
|
||||
AND (cert.auto_renew = 1 OR cert.not_after IS NULL)
|
||||
AND cert.renewing = 0
|
||||
AND cert.renew_failed = 0
|
||||
AND (cert.renew_retry IS NULL OR DATETIME() > DATETIME(cert.renew_retry))
|
||||
AND (cert.not_after IS NULL OR DATETIME(cert.not_after, 'utc', '-30 days') < DATETIME())
|
||||
ORDER BY cert.temp_parent, cert.not_after DESC NULLS FIRST
|
||||
LIMIT 1;
|
||||
@ -15,7 +15,7 @@ SELECT cert.id,
|
||||
cert.auto_renew,
|
||||
cert.active,
|
||||
cert.renewing,
|
||||
cert.renew_failed,
|
||||
cert.renew_retry,
|
||||
cert.not_after,
|
||||
cert.updated_at,
|
||||
certificate_domains.domain
|
||||
@ -24,14 +24,19 @@ FROM certificates AS cert
|
||||
|
||||
-- name: UpdateRenewingState :exec
|
||||
UPDATE certificates
|
||||
SET renewing = ?,
|
||||
renew_failed = ?
|
||||
SET renewing = ?,
|
||||
renew_retry = ?
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: SetRetryFlag :exec
|
||||
UPDATE certificates
|
||||
SET renew_retry = DATETIME('now', '+1 day')
|
||||
WHERE id = ?;
|
||||
|
||||
-- name: UpdateCertAfterRenewal :exec
|
||||
UPDATE certificates
|
||||
SET renewing = 0,
|
||||
renew_failed=0,
|
||||
SET renewing = 0,
|
||||
renew_retry=0,
|
||||
not_after=?,
|
||||
updated_at=?
|
||||
WHERE id = ?;
|
||||
|
@ -3,12 +3,15 @@ package database
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
)
|
||||
|
||||
var errMissingSqlDB = errors.New("cannot open transaction without sql.DB")
|
||||
|
||||
func (q *Queries) UseTx(ctx context.Context, cb func(tx *Queries) error) error {
|
||||
sqlDB, ok := q.db.(*sql.DB)
|
||||
if !ok {
|
||||
panic("cannot open transaction without sql.DB")
|
||||
return errMissingSqlDB
|
||||
}
|
||||
tx, err := sqlDB.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
|
56
go.mod
56
go.mod
@ -1,23 +1,24 @@
|
||||
module github.com/1f349/orchid
|
||||
|
||||
go 1.22
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/1f349/mjwt v0.4.1
|
||||
github.com/1f349/violet v0.0.14
|
||||
github.com/AlecAivazis/survey/v2 v2.3.7
|
||||
github.com/charmbracelet/log v0.4.0
|
||||
github.com/go-acme/lego/v4 v4.17.3
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1
|
||||
github.com/google/subcommands v1.2.0
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0
|
||||
github.com/charmbracelet/log v0.4.1
|
||||
github.com/go-acme/lego/v4 v4.22.2
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/julienschmidt/httprouter v1.3.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/miekg/dns v1.1.61
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/miekg/dns v1.1.64
|
||||
github.com/mrmelon54/certgen v0.0.2
|
||||
github.com/mrmelon54/exit-reload v0.0.2
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.36.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
@ -26,34 +27,37 @@ require (
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/becheran/wildmatch-go v1.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/charmbracelet/lipgloss v0.12.1 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.1.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/muesli/termenv v0.15.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.2.1 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
go.uber.org/atomic v1.11.0 // indirect
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect
|
||||
golang.org/x/mod v0.20.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/term v0.23.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/tools v0.24.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/sync v0.12.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
)
|
||||
|
108
go.sum
108
go.sum
@ -12,37 +12,44 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiE
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
|
||||
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0 h1:a9BinAjTfQh273eh7vd3qUgmBC+bx+3TRDtkZWmIpzM=
|
||||
github.com/bramvdbogaerde/go-scp v1.5.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs=
|
||||
github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8=
|
||||
github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8ZM=
|
||||
github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM=
|
||||
github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/log v0.4.1 h1:6AYnoHKADkghm/vt4neaNEXkxcXLSV2g1rdyFDOpTyk=
|
||||
github.com/charmbracelet/log v0.4.1/go.mod h1:pXgyTsqsVu4N9hGdHmQ0xEA4RsXof402LX9ZgiITn2I=
|
||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI=
|
||||
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-acme/lego/v4 v4.17.3 h1:5our7Qdyik0abag40abdmQuytq97iweaNHFMT4pYDnQ=
|
||||
github.com/go-acme/lego/v4 v4.17.3/go.mod h1:Ol6l04hnmavqVHKYS/ByhXXqE64x8yVYhomha82uAUk=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E=
|
||||
github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-acme/lego/v4 v4.22.2 h1:ck+HllWrV/rZGeYohsKQ5iKNnU/WAZxwOdiu6cxky+0=
|
||||
github.com/go-acme/lego/v4 v4.22.2/go.mod h1:E2FndyI3Ekv0usNJt46mFb9LVpV/XBYT+4E3tz02Tzo=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
|
||||
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
|
||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8=
|
||||
github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
@ -65,91 +72,92 @@ github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/miekg/dns v1.1.64 h1:wuZgD9wwCE6XMT05UU/mlSko71eRSXEAm2EbjQXLKnQ=
|
||||
github.com/miekg/dns v1.1.64/go.mod h1:Dzw9769uoKVaLuODMDZz9M6ynFU6Em65csPuoi8G0ck=
|
||||
github.com/mrmelon54/certgen v0.0.2 h1:4CMDkA/gGZu+E4iikU+5qdOWK7qOQrk58KtUfnmyYmY=
|
||||
github.com/mrmelon54/certgen v0.0.2/go.mod h1:vwrWSXQmxZYqEyh+cf05IvDIFV2aYuxL4+O6ABIlN8M=
|
||||
github.com/mrmelon54/exit-reload v0.0.2 h1:vqgfrMD/bF21HkDsWgg5+NLjFDrD3KGVEN/iTrMn9Ms=
|
||||
github.com/mrmelon54/exit-reload v0.0.2/go.mod h1:aE3NhsqGMLUqmv6cJZRouC/8gXkZTvVSabRGOpI+Vjc=
|
||||
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
|
||||
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/nrdcg/namesilo v0.2.1 h1:kLjCjsufdW/IlC+iSfAqj0iQGgKjlbUUeDJio5Y6eMg=
|
||||
github.com/nrdcg/namesilo v0.2.1/go.mod h1:lwMvfQTyYq+BbjJd30ylEG4GPSS6PII0Tia4rRpRiyw=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI=
|
||||
golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
|
||||
golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
|
||||
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU=
|
||||
golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.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.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
|
||||
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/1f349/orchid/database"
|
||||
"github.com/1f349/orchid/pebble"
|
||||
"github.com/1f349/orchid/utils"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
@ -201,6 +202,8 @@ var ErrAlreadyRenewing = errors.New("already renewing")
|
||||
// renewalRoutine is the main loop which makes used of certTicker to constantly
|
||||
// check if the existing certificates are up-to-date.
|
||||
func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
|
||||
Logger.Debug("Starting renewalRoutine")
|
||||
|
||||
// Upon leaving the function stop the ticker and clear the WaitGroup.
|
||||
defer func() {
|
||||
s.certTicker.Stop()
|
||||
@ -212,8 +215,7 @@ func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
|
||||
Logger.Info("Doing quick certificate check before starting...")
|
||||
err := s.renewalCheck()
|
||||
if err != nil {
|
||||
Logger.Info("Certificate check, should not error first try: ", "err", err)
|
||||
return
|
||||
Logger.Info("Certificate check, should not error first try", "err", err)
|
||||
}
|
||||
|
||||
// Logging or something
|
||||
@ -226,12 +228,14 @@ func (s *Service) renewalRoutine(wg *sync.WaitGroup) {
|
||||
// Exit if certDone has closed
|
||||
return
|
||||
case <-s.certTicker.C:
|
||||
Logger.Debug("Ticking certificate renewal")
|
||||
|
||||
// Start a new check in a separate routine
|
||||
go func() {
|
||||
// run a renewal check and log errors, but ignore ErrAlreadyRenewing
|
||||
err := s.renewalCheck()
|
||||
if err != nil && !errors.Is(err, ErrAlreadyRenewing) {
|
||||
Logger.Info("Certificate check, an error occurred: ", "err", err)
|
||||
Logger.Info("Certificate check, an error occurred", "err", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@ -246,6 +250,8 @@ func (s *Service) renewalCheck() error {
|
||||
}
|
||||
defer s.renewLock.Unlock()
|
||||
|
||||
Logger.Debug("Running renewalCheck")
|
||||
|
||||
// check for running out certificates in the database
|
||||
localData, err := s.findNextCertificateToRenew()
|
||||
if err != nil {
|
||||
@ -254,12 +260,14 @@ func (s *Service) renewalCheck() error {
|
||||
|
||||
// no certificates to update
|
||||
if localData == nil {
|
||||
Logger.Debug("No certificates to update")
|
||||
return nil
|
||||
}
|
||||
|
||||
// renew the certificate from the collected data
|
||||
err = s.renewCert(localData)
|
||||
if err != nil {
|
||||
Logger.Debug("Failed to renew certificate", "err", err)
|
||||
return err
|
||||
}
|
||||
|
||||
@ -285,7 +293,7 @@ func (s *Service) findNextCertificateToRenew() (*localCertData, error) {
|
||||
d.id = row.ID
|
||||
d.dns.name = row.Type
|
||||
d.dns.token = row.Token
|
||||
d.notAfter = row.NotAfter
|
||||
d.notAfter = row.NotAfter.Time
|
||||
d.tempParent = row.TempParent
|
||||
|
||||
return d, nil
|
||||
@ -315,6 +323,7 @@ func (s *Service) setupLegoClient() (*lego.Client, error) {
|
||||
}
|
||||
|
||||
// create lego client from the config
|
||||
// TODO: use a custom logger via `github.com/go-acme/lego/v4/log.Logger`
|
||||
client, err := lego.NewClient(config)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate client: %w", err)
|
||||
@ -360,7 +369,7 @@ func (s *Service) getDnsProvider(name, token string) (challenge.Provider, error)
|
||||
// getPrivateKey reads the private key for the specified certificate id, or
|
||||
// generates one is the file doesn't exist
|
||||
func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
|
||||
fPath := filepath.Join(s.keyDir, fmt.Sprintf("%d.key.pem", id))
|
||||
fPath := filepath.Join(s.keyDir, utils.GetKeyFileName(id))
|
||||
pemBytes, err := os.ReadFile(fPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@ -391,18 +400,20 @@ func (s *Service) getPrivateKey(id int64) (*rsa.PrivateKey, error) {
|
||||
// certificate to the certDir directory.
|
||||
func (s *Service) renewCert(localData *localCertData) error {
|
||||
// database synchronous state
|
||||
s.setRenewing(localData.id, true, false)
|
||||
s.setRenewing(localData.id, true)
|
||||
Logger.Debug("No certificates to update")
|
||||
|
||||
// run internal renewal code and log errors
|
||||
cert, certBytes, err := s.renewCertInternal(localData)
|
||||
if err != nil {
|
||||
s.setRenewing(localData.id, false, true)
|
||||
s.setRenewing(localData.id, false)
|
||||
s.setRetry(localData.id)
|
||||
return fmt.Errorf("failed to renew cert %d: %w", localData.id, err)
|
||||
}
|
||||
|
||||
// set the NotAfter/NotBefore in the database
|
||||
err = s.db.UpdateCertAfterRenewal(context.Background(), database.UpdateCertAfterRenewalParams{
|
||||
NotAfter: cert.NotAfter,
|
||||
NotAfter: sql.NullTime{Time: cert.NotAfter, Valid: true},
|
||||
UpdatedAt: cert.NotBefore,
|
||||
ID: localData.id,
|
||||
})
|
||||
@ -492,22 +503,28 @@ func (s *Service) renewCertInternal(localData *localCertData) (*x509.Certificate
|
||||
|
||||
// setRenewing sets the renewing and failed states in the database for a
|
||||
// specified certificate id.
|
||||
func (s *Service) setRenewing(id int64, renewing, failed bool) {
|
||||
func (s *Service) setRenewing(id int64, renewing bool) {
|
||||
err := s.db.UpdateRenewingState(context.Background(), database.UpdateRenewingStateParams{
|
||||
Renewing: renewing,
|
||||
RenewFailed: failed,
|
||||
ID: id,
|
||||
Renewing: renewing,
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
Logger.Warn("Failed to set renewing/failed mode in database", "id", id, "err", err)
|
||||
Logger.Warn("Failed to set renewing mode in database", "id", id, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) setRetry(id int64) {
|
||||
err := s.db.SetRetryFlag(context.Background(), id)
|
||||
if err != nil {
|
||||
Logger.Warn("Failed to set retry time in database", "id", id, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// writeCertFile writes the output certificate file and renames the current one
|
||||
// to include `-old` in the name.
|
||||
func (s *Service) writeCertFile(id int64, certBytes []byte) error {
|
||||
oldPath := filepath.Join(s.certDir, fmt.Sprintf("%d-old.cert.pem", id))
|
||||
newPath := filepath.Join(s.certDir, fmt.Sprintf("%d.cert.pem", id))
|
||||
oldPath := filepath.Join(s.certDir, utils.GetOldCertFileName(id))
|
||||
newPath := filepath.Join(s.certDir, utils.GetCertFileName(id))
|
||||
|
||||
// move certificate file to old name
|
||||
err := os.Rename(newPath, oldPath)
|
||||
|
@ -10,8 +10,10 @@ import (
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/1f349/orchid"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"github.com/1f349/orchid/pebble"
|
||||
"github.com/1f349/orchid/test"
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-acme/lego/v4/lego"
|
||||
"github.com/google/uuid"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
@ -71,7 +73,7 @@ func setupPebbleSuite(tb testing.TB) (*certgen.CertGen, func()) {
|
||||
assert.NoError(tb, os.WriteFile(filepath.Join(pebbleTmp, "certs", "localhost", "cert.pem"), serverTls.GetCertPem(), os.ModePerm))
|
||||
assert.NoError(tb, os.WriteFile(filepath.Join(pebbleTmp, "certs", "localhost", "key.pem"), serverTls.GetKeyPem(), os.ModePerm))
|
||||
|
||||
dnsServer := test.MakeFakeDnsProv("127.0.0.34:5053") // 127.0.0.34:53
|
||||
dnsServer := test.MakeFakeDnsProv("127.243.243.34:5053") // 127.243.243.34:53
|
||||
dnsServer.AddRecursiveSOA("example.test.")
|
||||
go dnsServer.Start()
|
||||
testDnsOptions = dnsServer
|
||||
@ -140,6 +142,8 @@ func deconstructPebbleTest(t *testing.T, service *Service) {
|
||||
}
|
||||
|
||||
func TestPebbleRenewal(t *testing.T) {
|
||||
logger.Logger.SetLevel(log.DebugLevel)
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping renewal tests in short mode")
|
||||
}
|
||||
@ -166,7 +170,7 @@ func TestPebbleRenewal(t *testing.T) {
|
||||
_, err := db2.Exec("DELETE FROM certificate_domains")
|
||||
assert.NoError(t, err)
|
||||
|
||||
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_failed, not_after, updated_at) VALUES (1, 1, 1, 1, 0, 0, "2000-01-01 00:00:00+00:00", "2000-01-01 00:00:00+00:00")`)
|
||||
_, err = db2.Exec(`INSERT INTO certificates (owner, dns, auto_renew, active, renewing, renew_retry, not_after, updated_at) VALUES (1, 1, 1, 1, 0, '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00');`)
|
||||
assert.NoError(t, err)
|
||||
for _, j := range i.domains {
|
||||
_, err = db2.Exec(`INSERT INTO certificate_domains (cert_id, domain) VALUES (1, ?)`, j)
|
||||
|
@ -25,14 +25,14 @@ type DomainStateValue struct {
|
||||
}
|
||||
|
||||
type Certificate struct {
|
||||
Id int64 `json:"id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewFailed bool `json:"renew_failed"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Domains []string `json:"domains"`
|
||||
Id int64 `json:"id"`
|
||||
AutoRenew bool `json:"auto_renew"`
|
||||
Active bool `json:"active"`
|
||||
Renewing bool `json:"renewing"`
|
||||
RenewRetry time.Time `json:"renew_retry"`
|
||||
NotAfter time.Time `json:"not_after"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
// NewApiServer creates and runs a http server containing all the API
|
||||
@ -69,13 +69,13 @@ func NewApiServer(listen string, db *database.Queries, signer *mjwt.KeyStore, do
|
||||
// loop over query rows
|
||||
for _, row := range rows {
|
||||
c := Certificate{
|
||||
Id: row.ID,
|
||||
AutoRenew: row.AutoRenew,
|
||||
Active: row.Active,
|
||||
Renewing: row.Renewing,
|
||||
RenewFailed: row.RenewFailed,
|
||||
NotAfter: row.NotAfter,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
Id: row.ID,
|
||||
AutoRenew: row.AutoRenew,
|
||||
Active: row.Active,
|
||||
Renewing: row.Renewing,
|
||||
RenewRetry: row.RenewRetry.Time,
|
||||
NotAfter: row.NotAfter.Time,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
d := row.Domain
|
||||
|
||||
@ -136,7 +136,7 @@ func NewApiServer(listen string, db *database.Queries, signer *mjwt.KeyStore, do
|
||||
err := db.AddCertificate(req.Context(), database.AddCertificateParams{
|
||||
Owner: b.Subject,
|
||||
Dns: sql.NullInt64{},
|
||||
NotAfter: time.Now(),
|
||||
NotAfter: sql.NullTime{Time: time.Now(), Valid: true},
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -8,3 +8,12 @@ sql:
|
||||
package: "database"
|
||||
out: "database"
|
||||
emit_json_tags: true
|
||||
overrides:
|
||||
- column: certificates.not_after
|
||||
go_type: database/sql.NullTime
|
||||
- column: certificates.renew_retry
|
||||
go_type: database/sql.NullTime
|
||||
- column: agents.last_sync
|
||||
go_type: database/sql.NullTime
|
||||
- column: agent_certs.not_after
|
||||
go_type: database/sql.NullTime
|
||||
|
@ -2,10 +2,10 @@ package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/1f349/orchid/logger"
|
||||
"github.com/go-acme/lego/v4/challenge"
|
||||
"github.com/go-acme/lego/v4/challenge/dns01"
|
||||
"github.com/miekg/dns"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@ -47,13 +47,13 @@ func (f *fakeDnsProv) AddRecursiveSOA(fqdn string) {
|
||||
func (f *fakeDnsProv) Present(domain, _, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
f.mTxt[info.EffectiveFQDN] = info.Value
|
||||
log.Printf("fakeDnsProv.Present(%s TXT %s)\n", info.EffectiveFQDN, info.Value)
|
||||
logger.Logger.Infof("fakeDnsProv.Present(%s TXT %s)", info.EffectiveFQDN, info.Value)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeDnsProv) CleanUp(domain, _, keyAuth string) error {
|
||||
info := dns01.GetChallengeInfo(domain, keyAuth)
|
||||
delete(f.mTxt, info.EffectiveFQDN)
|
||||
log.Printf("fakeDnsProv.CleanUp(%s TXT %s)\n", info.EffectiveFQDN, info.Value)
|
||||
logger.Logger.Infof("fakeDnsProv.CleanUp(%s TXT %s)", info.EffectiveFQDN, info.Value)
|
||||
return nil
|
||||
}
|
||||
func (f *fakeDnsProv) GetDnsAddrs() []string { return []string{f.Addr} }
|
||||
@ -62,7 +62,7 @@ func (f *fakeDnsProv) parseQuery(m *dns.Msg) {
|
||||
for _, q := range m.Question {
|
||||
switch q.Qtype {
|
||||
case dns.TypeTXT:
|
||||
log.Printf("Looking up %s TXT record\n", q.Name)
|
||||
logger.Logger.Info("Looking up TXT record", "name", q.Name)
|
||||
txt := f.mTxt[q.Name]
|
||||
if txt != "" {
|
||||
rr, err := dns.NewRR(fmt.Sprintf("%s 32600 IN TXT \"%s\"", q.Name, txt))
|
||||
@ -71,7 +71,7 @@ func (f *fakeDnsProv) parseQuery(m *dns.Msg) {
|
||||
}
|
||||
}
|
||||
default:
|
||||
log.Printf("Looking up %d for %s\n", q.Qtype, q.Name)
|
||||
logger.Logger.Info("Looking up", "qtype", q.Qtype, "name", q.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,10 +95,10 @@ func (f *fakeDnsProv) Start() {
|
||||
|
||||
// start server
|
||||
f.srv = &dns.Server{Addr: f.Addr, Net: "udp"}
|
||||
log.Printf("Starting fake dns service at %s\n", f.srv.Addr)
|
||||
logger.Logger.Info("Starting fake dns service", "addr", f.srv.Addr)
|
||||
err := f.srv.ListenAndServe()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to start server: %s\n ", err.Error())
|
||||
logger.Logger.Fatal("Failed to start server", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
15
utils/name.go
Normal file
15
utils/name.go
Normal file
@ -0,0 +1,15 @@
|
||||
package utils
|
||||
|
||||
import "fmt"
|
||||
|
||||
func GetCertFileName(id int64) string {
|
||||
return fmt.Sprintf("%d.cert.pem", id)
|
||||
}
|
||||
|
||||
func GetOldCertFileName(id int64) string {
|
||||
return fmt.Sprintf("%d-old.cert.pem", id)
|
||||
}
|
||||
|
||||
func GetKeyFileName(id int64) string {
|
||||
return fmt.Sprintf("%d.key.pem", id)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user