prepare for agent mode

This commit is contained in:
d34dscene
2024-10-03 17:58:01 +02:00
parent bb0841886f
commit b59d2e0297
27 changed files with 1870 additions and 324 deletions

View File

@@ -1,69 +0,0 @@
# Disable for now
# name: Docker Build
#
# on:
# push:
# branches:
# - main
# tags:
# - 'v*'
# pull_request:
# branches:
# - main
#
# jobs:
# build:
# runs-on: ubuntu-latest
#
# permissions:
# contents: read
# packages: write
# attestations: write
# id-token: write
#
# steps:
# - name: Checkout
# uses: actions/checkout@v4
#
# - name: Set up Node
# uses: actions/setup-node@v4
# with:
# node-version: 20.x
#
# - name: Set up QEMU
# uses: docker/setup-qemu-action@v3
#
# - name: Set up Docker Buildx
# uses: docker/setup-buildx-action@v3
#
# - name: Install dependencies
# run: |
# cd web
# npm install
# npm run build
#
# - name: Login to GitHub Container Registry
# uses: docker/login-action@v3
# with:
# registry: ghcr.io
# username: ${{ github.actor }}
# password: ${{ secrets.GITHUB_TOKEN }}
#
# - name: Extract metadata (tags, labels) for Docker
# id: meta
# uses: docker/metadata-action@v5
# with:
# images: |
# ghcr.io/${{ github.repository }}
# tags: |
# type=raw,value=latest,enable={{is_default_branch}}
# type=semver,pattern={{version}}
#
# - name: Build and push
# uses: docker/build-push-action@v6
# with:
# context: .
# platforms: linux/amd64
# push: ${{ github.event_name != 'pull_request' }}
# tags: ${{ steps.meta.outputs.tags }}
# labels: ${{ steps.meta.outputs.labels }}

View File

@@ -13,14 +13,14 @@ steps:
- pnpm install - pnpm install
- pnpm build - pnpm build
# - name: backend-audit - name: backend-audit
# image: golang:latest image: golang:latest
# commands: commands:
# - go fmt ./... - go fmt ./...
# - go vet ./... - go vet ./...
# - go mod tidy - go mod tidy
# - go mod verify - go mod verify
# - go test ./... - go test ./...
- name: container-build - name: container-build
image: woodpeckerci/plugin-docker-buildx image: woodpeckerci/plugin-docker-buildx

206
agent/client/client.go Normal file
View File

@@ -0,0 +1,206 @@
package client
import (
"context"
"errors"
"log"
"log/slog"
"net/http"
"os"
"slices"
"strconv"
"strings"
"time"
"connectrpc.com/connect"
agentv1 "github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1"
"github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1/agentv1connect"
"github.com/MizuchiLabs/mantrae/pkg/util"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/client"
"google.golang.org/protobuf/types/known/timestamppb"
)
func Client(quit chan os.Signal) {
token := LoadToken()
// Decode the JWT to get auth token and server URL
claims, err := util.DecodeJWT(token)
if err != nil {
DeleteToken()
log.Fatalf("Failed to decode JWT: %v", err)
}
// Create a new client instance
client := agentv1connect.NewAgentServiceClient(
http.DefaultClient,
claims.ServerURL,
connect.WithGRPC(),
)
slog.Info("Connected to", "server", claims.ServerURL)
// Start a goroutine for sending container data
go func() {
for {
// Send machine/container info
if _, err := client.GetContainer(context.Background(), sendContainer(claims.Secret)); err != nil {
slog.Error(
"Failed to send container info",
"server",
claims.ServerURL,
"error",
err,
)
}
// Wait 10 seconds before sending the next container info
time.Sleep(10 * time.Second)
}
}()
// Start a separate goroutine for refreshing the token
go func() {
for {
// Refresh token
tokenRequest := connect.NewRequest(&agentv1.RefreshTokenRequest{Token: token})
tokenRequest.Header().Set("Authorization", "Bearer "+claims.Secret)
newToken, err := client.RefreshToken(context.Background(), tokenRequest)
if err != nil {
slog.Error("Failed to refresh token", "server", claims.ServerURL, "error", err)
} else {
SaveToken(newToken.Msg.Token)
token = newToken.Msg.Token
}
time.Sleep(1 * time.Hour)
}
}()
// Wait for the main loop to finish
<-quit
}
func LoadToken() string {
token, err := os.ReadFile("token")
if err != nil {
slog.Error("Failed to read token", "error", err)
}
return strings.TrimSpace(string(token))
}
func SaveToken(token string) {
err := os.WriteFile("token", []byte(token), 0644)
if err != nil {
slog.Error("Failed to write token", "error", err)
}
}
func DeleteToken() {
err := os.Remove("token")
if err != nil {
slog.Error("Failed to delete token", "error", err)
}
}
// sendContainer creates a GetContainerRequest with information about the local machine
func sendContainer(secret string) *connect.Request[agentv1.GetContainerRequest] {
var request agentv1.GetContainerRequest
// Get machine ID
machineID, err := os.ReadFile("/etc/machine-id")
if err != nil {
request.Id = "unknown"
}
if len(machineID) > 0 {
request.Id = strings.TrimSpace(string(machineID))
}
// Get hostname
hostname, err := os.Hostname()
if err != nil {
request.Hostname = "unknown"
}
request.Hostname = hostname
request.PublicIp, err = util.GetPublicIP()
if err != nil {
slog.Error("Failed to get public IP", "error", err)
}
request.PrivateIps, err = util.GetPrivateIP()
if err != nil {
slog.Error("Failed to get local IP", "error", err)
}
request.Containers, err = getContainers()
if err != nil {
slog.Error("Failed to get containers", "error", err)
}
request.LastSeen = timestamppb.New(time.Now())
req := connect.NewRequest(&request)
req.Header().Set("authorization", "Bearer "+secret)
return req
}
// getContainers retrieves all containers and their info on the local machine
func getContainers() ([]*agentv1.Container, error) {
// Create a new Docker client
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return nil, errors.New("failed to create Docker client")
}
// Get all containers
containers, err := cli.ContainerList(context.Background(), container.ListOptions{})
if err != nil {
return nil, errors.New("failed to list containers")
}
var result []*agentv1.Container
// Iterate over each container and populate the Container struct
for _, c := range containers {
// Retrieve the container details
containerJSON, err := cli.ContainerInspect(context.Background(), c.ID)
if err != nil {
slog.Error("Failed to inspect container", "container", c.ID, "error", err)
continue
}
// Populate PortInfo
var ports []int32
for _, portmap := range containerJSON.NetworkSettings.Ports {
for _, binding := range portmap {
port, err := strconv.ParseInt(binding.HostPort, 10, 32)
if err != nil {
slog.Error("Failed to parse port", "port", port, "error", err)
continue
}
ports = append(ports, int32(port))
}
}
// Remove duplicates
slices.Sort(ports)
ports = slices.Compact(ports)
created, err := time.Parse(time.RFC3339, containerJSON.Created)
if err != nil {
slog.Error("Failed to parse created time", "time", containerJSON.Created, "error", err)
}
// Populate the Container struct
container := &agentv1.Container{
Id: c.ID,
Name: c.Names[0], // Take the first name if multiple exist
Labels: containerJSON.Config.Labels,
Image: containerJSON.Config.Image,
Ports: ports,
Status: containerJSON.State.Status,
Created: timestamppb.New(created),
}
result = append(result, container)
}
return result, nil
}

48
agent/cmd/main.go Normal file
View File

@@ -0,0 +1,48 @@
package main
import (
"flag"
"log"
"log/slog"
"os"
"os/signal"
"syscall"
"github.com/MizuchiLabs/mantrae/agent/client"
"github.com/lmittmann/tint"
)
// Set up global logger
func init() {
logger := slog.New(tint.NewHandler(os.Stdout, nil))
slog.SetDefault(logger)
}
func main() {
token := flag.String("token", "", "Authentication token (required)")
// update := flag.Bool("update", false, "Update to latest version")
version := flag.Bool("version", false, "Show version")
flag.Parse()
if *token == "" {
*token = client.LoadToken()
if len(*token) == 0 {
slog.Error("missing token")
return
}
} else {
client.SaveToken(*token)
}
if *version {
log.Println("v0.0.1")
return
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
slog.Info("Starting agent...")
client.Client(quit)
}

View File

@@ -0,0 +1,37 @@
syntax = "proto3";
package agent.v1;
import "google/protobuf/timestamp.proto";
service AgentService {
rpc GetContainer(GetContainerRequest) returns (GetContainerResponse);
rpc RefreshToken(RefreshTokenRequest) returns (RefreshTokenResponse);
}
message Container {
string id = 1;
string name = 2;
map<string, string> labels = 3;
string image = 4;
repeated int32 ports = 5;
string status = 6;
google.protobuf.Timestamp created = 7;
}
message GetContainerRequest {
string id = 1;
string hostname = 2;
string public_ip = 3;
repeated string private_ips = 4;
repeated Container containers = 5;
google.protobuf.Timestamp last_seen = 6;
}
message GetContainerResponse {}
message RefreshTokenRequest {
string token = 1;
}
message RefreshTokenResponse {
string token = 1;
}

13
agent/proto/buf.gen.yaml Normal file
View File

@@ -0,0 +1,13 @@
version: v2
managed:
enabled: true
override:
- file_option: go_package_prefix
value: github.com/MizuchiLabs/mantrae/agent/proto/gen
plugins:
- local: protoc-gen-go
out: gen
opt: paths=source_relative
- local: protoc-gen-connect-go
out: gen
opt: paths=source_relative

8
agent/proto/buf.yaml Normal file
View File

@@ -0,0 +1,8 @@
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
use:
- STANDARD
breaking:
use:
- FILE

View File

@@ -0,0 +1,532 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2
// protoc (unknown)
// source: agent/v1/agent.proto
package agentv1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Container struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Labels map[string]string `protobuf:"bytes,3,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
Image string `protobuf:"bytes,4,opt,name=image,proto3" json:"image,omitempty"`
Ports []int32 `protobuf:"varint,5,rep,packed,name=ports,proto3" json:"ports,omitempty"`
Status string `protobuf:"bytes,6,opt,name=status,proto3" json:"status,omitempty"`
Created *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=created,proto3" json:"created,omitempty"`
}
func (x *Container) Reset() {
*x = Container{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_v1_agent_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Container) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Container) ProtoMessage() {}
func (x *Container) ProtoReflect() protoreflect.Message {
mi := &file_agent_v1_agent_proto_msgTypes[0]
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 Container.ProtoReflect.Descriptor instead.
func (*Container) Descriptor() ([]byte, []int) {
return file_agent_v1_agent_proto_rawDescGZIP(), []int{0}
}
func (x *Container) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *Container) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *Container) GetLabels() map[string]string {
if x != nil {
return x.Labels
}
return nil
}
func (x *Container) GetImage() string {
if x != nil {
return x.Image
}
return ""
}
func (x *Container) GetPorts() []int32 {
if x != nil {
return x.Ports
}
return nil
}
func (x *Container) GetStatus() string {
if x != nil {
return x.Status
}
return ""
}
func (x *Container) GetCreated() *timestamppb.Timestamp {
if x != nil {
return x.Created
}
return nil
}
type GetContainerRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"`
PublicIp string `protobuf:"bytes,3,opt,name=public_ip,json=publicIp,proto3" json:"public_ip,omitempty"`
PrivateIps []string `protobuf:"bytes,4,rep,name=private_ips,json=privateIps,proto3" json:"private_ips,omitempty"`
Containers []*Container `protobuf:"bytes,5,rep,name=containers,proto3" json:"containers,omitempty"`
LastSeen *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=last_seen,json=lastSeen,proto3" json:"last_seen,omitempty"`
}
func (x *GetContainerRequest) Reset() {
*x = GetContainerRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_v1_agent_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetContainerRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetContainerRequest) ProtoMessage() {}
func (x *GetContainerRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_v1_agent_proto_msgTypes[1]
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 GetContainerRequest.ProtoReflect.Descriptor instead.
func (*GetContainerRequest) Descriptor() ([]byte, []int) {
return file_agent_v1_agent_proto_rawDescGZIP(), []int{1}
}
func (x *GetContainerRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *GetContainerRequest) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
func (x *GetContainerRequest) GetPublicIp() string {
if x != nil {
return x.PublicIp
}
return ""
}
func (x *GetContainerRequest) GetPrivateIps() []string {
if x != nil {
return x.PrivateIps
}
return nil
}
func (x *GetContainerRequest) GetContainers() []*Container {
if x != nil {
return x.Containers
}
return nil
}
func (x *GetContainerRequest) GetLastSeen() *timestamppb.Timestamp {
if x != nil {
return x.LastSeen
}
return nil
}
type GetContainerResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
}
func (x *GetContainerResponse) Reset() {
*x = GetContainerResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_v1_agent_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *GetContainerResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetContainerResponse) ProtoMessage() {}
func (x *GetContainerResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_v1_agent_proto_msgTypes[2]
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 GetContainerResponse.ProtoReflect.Descriptor instead.
func (*GetContainerResponse) Descriptor() ([]byte, []int) {
return file_agent_v1_agent_proto_rawDescGZIP(), []int{2}
}
type RefreshTokenRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
}
func (x *RefreshTokenRequest) Reset() {
*x = RefreshTokenRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_v1_agent_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RefreshTokenRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshTokenRequest) ProtoMessage() {}
func (x *RefreshTokenRequest) ProtoReflect() protoreflect.Message {
mi := &file_agent_v1_agent_proto_msgTypes[3]
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 RefreshTokenRequest.ProtoReflect.Descriptor instead.
func (*RefreshTokenRequest) Descriptor() ([]byte, []int) {
return file_agent_v1_agent_proto_rawDescGZIP(), []int{3}
}
func (x *RefreshTokenRequest) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
type RefreshTokenResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"`
}
func (x *RefreshTokenResponse) Reset() {
*x = RefreshTokenResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_agent_v1_agent_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *RefreshTokenResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RefreshTokenResponse) ProtoMessage() {}
func (x *RefreshTokenResponse) ProtoReflect() protoreflect.Message {
mi := &file_agent_v1_agent_proto_msgTypes[4]
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 RefreshTokenResponse.ProtoReflect.Descriptor instead.
func (*RefreshTokenResponse) Descriptor() ([]byte, []int) {
return file_agent_v1_agent_proto_rawDescGZIP(), []int{4}
}
func (x *RefreshTokenResponse) GetToken() string {
if x != nil {
return x.Token
}
return ""
}
var File_agent_v1_agent_proto protoreflect.FileDescriptor
var file_agent_v1_agent_proto_rawDesc = []byte{
0x0a, 0x14, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31,
0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x22, 0x9d, 0x02, 0x0a, 0x09, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12,
0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e,
0x61, 0x6d, 0x65, 0x12, 0x37, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x03, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43,
0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45,
0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x14, 0x0a, 0x05,
0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61,
0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28,
0x05, 0x52, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x12, 0x34, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63,
0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73,
0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01,
0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38,
0x01, 0x22, 0xed, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e,
0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73,
0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73,
0x74, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f,
0x69, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63,
0x49, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x70,
0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65,
0x49, 0x70, 0x73, 0x12, 0x33, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72,
0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e,
0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0a, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x37, 0x0a, 0x09, 0x6c, 0x61, 0x73, 0x74,
0x5f, 0x73, 0x65, 0x65, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x6c, 0x61, 0x73, 0x74, 0x53, 0x65, 0x65,
0x6e, 0x22, 0x16, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65,
0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x2b, 0x0a, 0x13, 0x52, 0x65, 0x66,
0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2c, 0x0a, 0x14, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73,
0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14,
0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74,
0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xac, 0x01, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x53, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74,
0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31,
0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x71,
0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e,
0x47, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70,
0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x0c, 0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54,
0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e,
0x52, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75,
0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52,
0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x42, 0x9c, 0x01, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x2e, 0x76, 0x31, 0x42, 0x0a, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x74, 0x6f,
0x50, 0x01, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4d,
0x69, 0x7a, 0x75, 0x63, 0x68, 0x69, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x6d, 0x61, 0x6e, 0x74, 0x72,
0x61, 0x65, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x67,
0x65, 0x6e, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x58, 0x58, 0xaa, 0x02, 0x08, 0x41, 0x67, 0x65, 0x6e,
0x74, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x08, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0xe2,
0x02, 0x14, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x09, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x3a, 0x3a,
0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_agent_v1_agent_proto_rawDescOnce sync.Once
file_agent_v1_agent_proto_rawDescData = file_agent_v1_agent_proto_rawDesc
)
func file_agent_v1_agent_proto_rawDescGZIP() []byte {
file_agent_v1_agent_proto_rawDescOnce.Do(func() {
file_agent_v1_agent_proto_rawDescData = protoimpl.X.CompressGZIP(file_agent_v1_agent_proto_rawDescData)
})
return file_agent_v1_agent_proto_rawDescData
}
var file_agent_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_agent_v1_agent_proto_goTypes = []any{
(*Container)(nil), // 0: agent.v1.Container
(*GetContainerRequest)(nil), // 1: agent.v1.GetContainerRequest
(*GetContainerResponse)(nil), // 2: agent.v1.GetContainerResponse
(*RefreshTokenRequest)(nil), // 3: agent.v1.RefreshTokenRequest
(*RefreshTokenResponse)(nil), // 4: agent.v1.RefreshTokenResponse
nil, // 5: agent.v1.Container.LabelsEntry
(*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp
}
var file_agent_v1_agent_proto_depIdxs = []int32{
5, // 0: agent.v1.Container.labels:type_name -> agent.v1.Container.LabelsEntry
6, // 1: agent.v1.Container.created:type_name -> google.protobuf.Timestamp
0, // 2: agent.v1.GetContainerRequest.containers:type_name -> agent.v1.Container
6, // 3: agent.v1.GetContainerRequest.last_seen:type_name -> google.protobuf.Timestamp
1, // 4: agent.v1.AgentService.GetContainer:input_type -> agent.v1.GetContainerRequest
3, // 5: agent.v1.AgentService.RefreshToken:input_type -> agent.v1.RefreshTokenRequest
2, // 6: agent.v1.AgentService.GetContainer:output_type -> agent.v1.GetContainerResponse
4, // 7: agent.v1.AgentService.RefreshToken:output_type -> agent.v1.RefreshTokenResponse
6, // [6:8] is the sub-list for method output_type
4, // [4:6] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_agent_v1_agent_proto_init() }
func file_agent_v1_agent_proto_init() {
if File_agent_v1_agent_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_agent_v1_agent_proto_msgTypes[0].Exporter = func(v any, i int) any {
switch v := v.(*Container); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_v1_agent_proto_msgTypes[1].Exporter = func(v any, i int) any {
switch v := v.(*GetContainerRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_v1_agent_proto_msgTypes[2].Exporter = func(v any, i int) any {
switch v := v.(*GetContainerResponse); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_v1_agent_proto_msgTypes[3].Exporter = func(v any, i int) any {
switch v := v.(*RefreshTokenRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_agent_v1_agent_proto_msgTypes[4].Exporter = func(v any, i int) any {
switch v := v.(*RefreshTokenResponse); 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{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_agent_v1_agent_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_agent_v1_agent_proto_goTypes,
DependencyIndexes: file_agent_v1_agent_proto_depIdxs,
MessageInfos: file_agent_v1_agent_proto_msgTypes,
}.Build()
File_agent_v1_agent_proto = out.File
file_agent_v1_agent_proto_rawDesc = nil
file_agent_v1_agent_proto_goTypes = nil
file_agent_v1_agent_proto_depIdxs = nil
}

View File

@@ -0,0 +1,143 @@
// Code generated by protoc-gen-connect-go. DO NOT EDIT.
//
// Source: agent/v1/agent.proto
package agentv1connect
import (
connect "connectrpc.com/connect"
context "context"
errors "errors"
v1 "github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1"
http "net/http"
strings "strings"
)
// This is a compile-time assertion to ensure that this generated file and the connect package are
// compatible. If you get a compiler error that this constant is not defined, this code was
// generated with a version of connect newer than the one compiled into your binary. You can fix the
// problem by either regenerating this code with an older version of connect or updating the connect
// version compiled into your binary.
const _ = connect.IsAtLeastVersion1_13_0
const (
// AgentServiceName is the fully-qualified name of the AgentService service.
AgentServiceName = "agent.v1.AgentService"
)
// These constants are the fully-qualified names of the RPCs defined in this package. They're
// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route.
//
// Note that these are different from the fully-qualified method names used by
// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to
// reflection-formatted method names, remove the leading slash and convert the remaining slash to a
// period.
const (
// AgentServiceGetContainerProcedure is the fully-qualified name of the AgentService's GetContainer
// RPC.
AgentServiceGetContainerProcedure = "/agent.v1.AgentService/GetContainer"
// AgentServiceRefreshTokenProcedure is the fully-qualified name of the AgentService's RefreshToken
// RPC.
AgentServiceRefreshTokenProcedure = "/agent.v1.AgentService/RefreshToken"
)
// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package.
var (
agentServiceServiceDescriptor = v1.File_agent_v1_agent_proto.Services().ByName("AgentService")
agentServiceGetContainerMethodDescriptor = agentServiceServiceDescriptor.Methods().ByName("GetContainer")
agentServiceRefreshTokenMethodDescriptor = agentServiceServiceDescriptor.Methods().ByName("RefreshToken")
)
// AgentServiceClient is a client for the agent.v1.AgentService service.
type AgentServiceClient interface {
GetContainer(context.Context, *connect.Request[v1.GetContainerRequest]) (*connect.Response[v1.GetContainerResponse], error)
RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error)
}
// NewAgentServiceClient constructs a client for the agent.v1.AgentService service. By default, it
// uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and sends
// uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() or
// connect.WithGRPCWeb() options.
//
// The URL supplied here should be the base URL for the Connect or gRPC server (for example,
// http://api.acme.com or https://acme.com/grpc).
func NewAgentServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AgentServiceClient {
baseURL = strings.TrimRight(baseURL, "/")
return &agentServiceClient{
getContainer: connect.NewClient[v1.GetContainerRequest, v1.GetContainerResponse](
httpClient,
baseURL+AgentServiceGetContainerProcedure,
connect.WithSchema(agentServiceGetContainerMethodDescriptor),
connect.WithClientOptions(opts...),
),
refreshToken: connect.NewClient[v1.RefreshTokenRequest, v1.RefreshTokenResponse](
httpClient,
baseURL+AgentServiceRefreshTokenProcedure,
connect.WithSchema(agentServiceRefreshTokenMethodDescriptor),
connect.WithClientOptions(opts...),
),
}
}
// agentServiceClient implements AgentServiceClient.
type agentServiceClient struct {
getContainer *connect.Client[v1.GetContainerRequest, v1.GetContainerResponse]
refreshToken *connect.Client[v1.RefreshTokenRequest, v1.RefreshTokenResponse]
}
// GetContainer calls agent.v1.AgentService.GetContainer.
func (c *agentServiceClient) GetContainer(ctx context.Context, req *connect.Request[v1.GetContainerRequest]) (*connect.Response[v1.GetContainerResponse], error) {
return c.getContainer.CallUnary(ctx, req)
}
// RefreshToken calls agent.v1.AgentService.RefreshToken.
func (c *agentServiceClient) RefreshToken(ctx context.Context, req *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) {
return c.refreshToken.CallUnary(ctx, req)
}
// AgentServiceHandler is an implementation of the agent.v1.AgentService service.
type AgentServiceHandler interface {
GetContainer(context.Context, *connect.Request[v1.GetContainerRequest]) (*connect.Response[v1.GetContainerResponse], error)
RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error)
}
// NewAgentServiceHandler builds an HTTP handler from the service implementation. It returns the
// path on which to mount the handler and the handler itself.
//
// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf
// and JSON codecs. They also support gzip compression.
func NewAgentServiceHandler(svc AgentServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) {
agentServiceGetContainerHandler := connect.NewUnaryHandler(
AgentServiceGetContainerProcedure,
svc.GetContainer,
connect.WithSchema(agentServiceGetContainerMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
agentServiceRefreshTokenHandler := connect.NewUnaryHandler(
AgentServiceRefreshTokenProcedure,
svc.RefreshToken,
connect.WithSchema(agentServiceRefreshTokenMethodDescriptor),
connect.WithHandlerOptions(opts...),
)
return "/agent.v1.AgentService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case AgentServiceGetContainerProcedure:
agentServiceGetContainerHandler.ServeHTTP(w, r)
case AgentServiceRefreshTokenProcedure:
agentServiceRefreshTokenHandler.ServeHTTP(w, r)
default:
http.NotFound(w, r)
}
})
}
// UnimplementedAgentServiceHandler returns CodeUnimplemented from all methods.
type UnimplementedAgentServiceHandler struct{}
func (UnimplementedAgentServiceHandler) GetContainer(context.Context, *connect.Request[v1.GetContainerRequest]) (*connect.Response[v1.GetContainerResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("agent.v1.AgentService.GetContainer is not implemented"))
}
func (UnimplementedAgentServiceHandler) RefreshToken(context.Context, *connect.Request[v1.RefreshTokenRequest]) (*connect.Response[v1.RefreshTokenResponse], error) {
return nil, connect.NewError(connect.CodeUnimplemented, errors.New("agent.v1.AgentService.RefreshToken is not implemented"))
}

26
go.mod
View File

@@ -7,6 +7,7 @@ require github.com/traefik/genconf v0.5.2
require ( require (
connectrpc.com/connect v1.17.0 connectrpc.com/connect v1.17.0
github.com/cloudflare/cloudflare-go v0.106.0 github.com/cloudflare/cloudflare-go v0.106.0
github.com/docker/docker v27.3.1+incompatible
github.com/golang-jwt/jwt/v5 v5.2.1 github.com/golang-jwt/jwt/v5 v5.2.1
github.com/joeig/go-powerdns/v3 v3.14.1 github.com/joeig/go-powerdns/v3 v3.14.1
github.com/lmittmann/tint v1.0.5 github.com/lmittmann/tint v1.0.5
@@ -14,19 +15,42 @@ require (
github.com/pressly/goose/v3 v3.22.1 github.com/pressly/goose/v3 v3.22.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
golang.org/x/crypto v0.27.0 golang.org/x/crypto v0.27.0
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8
golang.org/x/net v0.29.0 golang.org/x/net v0.29.0
google.golang.org/protobuf v1.34.2 google.golang.org/protobuf v1.34.2
sigs.k8s.io/yaml v1.4.0 sigs.k8s.io/yaml v1.4.0
) )
require ( require (
github.com/Microsoft/go-winio v0.4.14 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/goccy/go-json v0.10.3 // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect github.com/google/go-querystring v1.1.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect github.com/mfridman/interpolate v0.0.2 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect github.com/sethvargo/go-retry v0.3.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 // indirect
go.opentelemetry.io/otel v1.30.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect
go.opentelemetry.io/otel/metric v1.30.0 // indirect
go.opentelemetry.io/otel/sdk v1.30.0 // indirect
go.opentelemetry.io/otel/trace v1.30.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.8.0 // indirect golang.org/x/sync v0.8.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect golang.org/x/text v0.18.0 // indirect
golang.org/x/time v0.6.0 // indirect golang.org/x/time v0.6.0 // indirect
gotest.tools/v3 v3.5.1 // indirect
) )

106
go.sum
View File

@@ -1,29 +1,59 @@
connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk= connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.4.14 h1:+hMXMk01us9KgxGb7ftKQt2Xpf5hH/yky+TDA+qxleU=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
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/cloudflare/cloudflare-go v0.106.0 h1:q41gC5Wc1nfi0D1ZhSHokWcd9mGMbqC7RE7qiP+qE00= github.com/cloudflare/cloudflare-go v0.106.0 h1:q41gC5Wc1nfi0D1ZhSHokWcd9mGMbqC7RE7qiP+qE00=
github.com/cloudflare/cloudflare-go v0.106.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM= github.com/cloudflare/cloudflare-go v0.106.0/go.mod h1:pfUQ4PIG4ISI0/Mmc21Bp86UnFU0ktmPf3iTgbSL+cM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 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/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/joeig/go-powerdns/v3 v3.14.1 h1:ff+ClS/yM5ZBigh5oe4m0T/Na2k0k+JNpyuby0LkGCc= github.com/joeig/go-powerdns/v3 v3.14.1 h1:ff+ClS/yM5ZBigh5oe4m0T/Na2k0k+JNpyuby0LkGCc=
github.com/joeig/go-powerdns/v3 v3.14.1/go.mod h1:hA54LX2p4A/Jp1Kgdhd7Lh3jAU0u7wV1mk1JbGywt60= github.com/joeig/go-powerdns/v3 v3.14.1/go.mod h1:hA54LX2p4A/Jp1Kgdhd7Lh3jAU0u7wV1mk1JbGywt60=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -36,8 +66,21 @@ github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWt
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY=
github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug=
github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 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.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc= github.com/pressly/goose/v3 v3.22.1 h1:2zICEfr1O3yTP9BRZMGPj7qFxQ+ik6yeo+z1LMuioLc=
@@ -50,25 +93,82 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE=
github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 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.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/traefik/genconf v0.5.2 h1:R90hthLOCU15OwVa1p5gbqkKdpXj77iIk7Ysk2r6beg= github.com/traefik/genconf v0.5.2 h1:R90hthLOCU15OwVa1p5gbqkKdpXj77iIk7Ysk2r6beg=
github.com/traefik/genconf v0.5.2/go.mod h1:D3wUYg0jGF3ScEMOaNOXlqobGDiIbpr4QEwWK3HCQlg= github.com/traefik/genconf v0.5.2/go.mod h1:D3wUYg0jGF3ScEMOaNOXlqobGDiIbpr4QEwWK3HCQlg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0 h1:ZIg3ZT/aQ7AfKqdwp7ECpOK6vHqquXXuyTjIO8ZdmPs=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.55.0/go.mod h1:DQAwmETtZV00skUwgD6+0U89g80NKsJE3DCKeLLPQMI=
go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts=
go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8=
go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w=
go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ=
go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE=
go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg=
go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc=
go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw=
golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 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.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
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.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/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= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc=
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM=
google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@@ -76,6 +176,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 h1:5D53IMaUuA5InSeMu9eJtlQXS2NxAhyWQvkKEgXZhHI=
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4= modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6/go.mod h1:Qz0X07sNOR1jWYCrJMEnbW/X55x206Q7Vt4mz6/wHp4=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=

View File

@@ -2,22 +2,48 @@ package api
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"log/slog"
"net/http" "net/http"
"os"
"strings" "strings"
"sync" "sync"
"time"
"connectrpc.com/connect" "connectrpc.com/connect"
"golang.org/x/net/http2"
"golang.org/x/net/http2/h2c"
agentv1 "github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1" agentv1 "github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1"
"github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1/agentv1connect" "github.com/MizuchiLabs/mantrae/agent/proto/gen/agent/v1/agentv1connect"
"github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/pkg/util"
) )
type AgentServer struct { type AgentServer struct {
mu sync.Mutex mu sync.Mutex
agents map[string]agentv1.GetContainerRequest }
func (s *AgentServer) RefreshToken(
ctx context.Context,
req *connect.Request[agentv1.RefreshTokenRequest],
) (*connect.Response[agentv1.RefreshTokenResponse], error) {
if err := validate(req.Header()); err != nil {
return nil, err
}
decoded, err := util.DecodeJWT(req.Msg.GetToken())
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
token, err := util.EncodeAgentJWT(decoded.ServerURL)
if err != nil {
return nil, connect.NewError(connect.CodeInternal, err)
}
return connect.NewResponse(&agentv1.RefreshTokenResponse{
Token: token,
}), nil
} }
func (s *AgentServer) GetContainer( func (s *AgentServer) GetContainer(
@@ -28,22 +54,33 @@ func (s *AgentServer) GetContainer(
return nil, err return nil, err
} }
// Add agent // Upsert agent
s.mu.Lock() s.mu.Lock()
s.agents[req.Msg.GetId()] = agentv1.GetContainerRequest{ privateIpsJSON, err := json.Marshal(req.Msg.GetPrivateIps())
Id: req.Msg.GetId(), if err != nil {
s.mu.Unlock()
return nil, connect.NewError(connect.CodeInternal, err)
}
containersJSON, err := json.Marshal(req.Msg.GetContainers())
if err != nil {
s.mu.Unlock()
return nil, connect.NewError(connect.CodeInternal, err)
}
lastSeen := req.Msg.GetLastSeen().AsTime()
if _, err := db.Query.UpsertAgent(context.Background(), db.UpsertAgentParams{
ID: req.Msg.GetId(),
Hostname: req.Msg.GetHostname(), Hostname: req.Msg.GetHostname(),
Containers: req.Msg.GetContainers(), PublicIp: &req.Msg.PublicIp,
LastSeen: req.Msg.GetLastSeen(), PrivateIps: privateIpsJSON,
Containers: containersJSON,
LastSeen: &lastSeen,
}); err != nil {
s.mu.Unlock()
return nil, connect.NewError(connect.CodeInternal, err)
} }
s.mu.Unlock() s.mu.Unlock()
return connect.NewResponse(&agentv1.GetContainerResponse{}), nil
}
func (s *AgentServer) deleteAgent(id string) { return connect.NewResponse(&agentv1.GetContainerResponse{}), nil
s.mu.Lock()
defer s.mu.Unlock()
delete(s.agents, id)
} }
func validate(header http.Header) error { func validate(header http.Header) error {
@@ -54,10 +91,7 @@ func validate(header http.Header) error {
if !strings.HasPrefix(auth, "Bearer ") { if !strings.HasPrefix(auth, "Bearer ") {
return connect.NewError(connect.CodeUnauthenticated, errors.New("missing bearer prefix")) return connect.NewError(connect.CodeUnauthenticated, errors.New("missing bearer prefix"))
} }
if strings.TrimSpace(os.Getenv("SECRET")) != strings.TrimPrefix(auth, "Bearer ") {
token := "test"
if strings.TrimSpace(string(token)) != strings.TrimPrefix(auth, "Bearer ") {
return connect.NewError( return connect.NewError(
connect.CodeUnauthenticated, connect.CodeUnauthenticated,
errors.New("failed to validate token"), errors.New("failed to validate token"),
@@ -67,15 +101,35 @@ func validate(header http.Header) error {
return nil return nil
} }
func Server() { func Server(port string) {
agent := &AgentServer{} agent := &AgentServer{}
mux := http.NewServeMux() mux := http.NewServeMux()
path, handler := agentv1connect.NewAgentServiceHandler(agent) path, handler := agentv1connect.NewAgentServiceHandler(agent)
mux.Handle(path, handler) mux.Handle(path, handler)
http.ListenAndServe( if port == "" {
":8080", port = ":8090"
h2c.NewHandler(mux, &http2.Server{}), } else if !strings.HasPrefix(port, ":") {
) port = ":" + port
}
srv := &http.Server{
Addr: port,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
MaxHeaderBytes: 1 << 20, // 1MB
}
slog.Info("gRPC server running on", "port", port)
go func() {
if err := srv.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
slog.Info("gRPC server closed")
return
}
slog.Error("gRPC server error", "err", err)
return
}
}()
} }

View File

@@ -1,57 +0,0 @@
package api
import (
"fmt"
"os"
"time"
"github.com/golang-jwt/jwt/v5"
)
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
// GenerateJWT generates a JWT token
func GenerateJWT(username string) (string, error) {
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
},
}
if username == "" {
return "", nil
}
secret := os.Getenv("SECRET")
if secret == "" {
return "", fmt.Errorf("SECRET environment variable is not set")
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// ValidateJWT validates a JWT token
func ValidateJWT(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(
tokenString,
claims,
func(token *jwt.Token) (interface{}, error) {
return []byte(os.Getenv("SECRET")), nil
},
)
if err != nil {
return nil, err
}
if !token.Valid {
return nil, err
}
return claims, nil
}

View File

@@ -1,156 +0,0 @@
package api
import (
"os"
"reflect"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
)
func TestGenerateJWT(t *testing.T) {
type args struct {
username string
}
tests := []struct {
name string
args args
emptyToken bool
wantErr bool
setEnv bool
}{
{
name: "Valid Username",
args: args{
username: "testuser",
},
emptyToken: false,
wantErr: false,
setEnv: true,
},
{
name: "Empty Username",
args: args{
username: "",
},
emptyToken: true,
wantErr: false,
setEnv: true,
},
{
name: "Without Secret",
args: args{
username: "testuser",
},
emptyToken: false,
wantErr: true,
setEnv: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setEnv {
os.Setenv("SECRET", "dummy-secret") // Set the secret environment variable
} else {
os.Unsetenv("SECRET")
}
token, err := GenerateJWT(tt.args.username)
if (err != nil) != tt.wantErr {
t.Errorf("GenerateJWT() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.emptyToken {
if token != "" {
t.Errorf("GenerateJWT() = %v, want empty string", token)
}
}
})
}
}
func TestValidateJWT(t *testing.T) {
os.Setenv("SECRET", "dummy-secret") // Set the secret environment variable
validToken, _ := GenerateJWT("testuser")
type args struct {
tokenString string
}
tests := []struct {
name string
args args
want *Claims
wantErr bool
setEnv bool
}{
{
name: "Valid Token",
args: args{
tokenString: validToken,
},
want: &Claims{
Username: "testuser",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
},
},
wantErr: false,
setEnv: true,
},
{
name: "Invalid Token",
args: args{
tokenString: "invalidTokenString",
},
want: nil,
wantErr: true,
setEnv: true,
},
{
name: "Expired Token",
args: args{
tokenString: func() string {
claims := &Claims{
Username: "expireduser",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(-24 * time.Hour)),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, _ := token.SignedString([]byte(os.Getenv("SECRET")))
return tokenString
}(),
},
want: nil,
wantErr: true,
setEnv: true,
},
{
name: "Without Secret",
args: args{
tokenString: "invalidTokenString",
},
want: nil,
wantErr: true,
setEnv: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.setEnv {
os.Setenv("SECRET", "dummy-secret") // Set the secret environment variable
} else {
os.Unsetenv("SECRET")
}
got, err := ValidateJWT(tt.args.tokenString)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateJWT() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ValidateJWT() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -55,7 +55,7 @@ func Login(w http.ResponseWriter, r *http.Request) {
return return
} }
token, err := GenerateJWT(user.Username) token, err := util.EncodeUserJWT(user.Username)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError) http.Error(w, err.Error(), http.StatusInternalServerError)
return return
@@ -71,7 +71,7 @@ func VerifyToken(w http.ResponseWriter, r *http.Request) {
return return
} }
_, err := ValidateJWT(tokenString[7:]) _, err := util.DecodeJWT(tokenString[7:])
if err != nil { if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized) http.Error(w, "Invalid token", http.StatusUnauthorized)
return return
@@ -121,6 +121,61 @@ func GetEvents(w http.ResponseWriter, r *http.Request) {
} }
} }
// Agents ---------------------------------------------------------------------
// GetAgents returns all agents
func GetAgents(w http.ResponseWriter, r *http.Request) {
agents, err := db.Query.ListAgents(context.Background())
if err != nil {
http.Error(w, "Failed to get agents", http.StatusInternalServerError)
return
}
writeJSON(w, agents)
}
// GetAgent returns an agent
func GetAgent(w http.ResponseWriter, r *http.Request) {
agent, err := db.Query.GetAgentByID(context.Background(), r.PathValue("id"))
if err != nil {
http.Error(w, "Failed to get agent", http.StatusInternalServerError)
return
}
writeJSON(w, agent)
}
// DeleteAgent deletes an agent
func DeleteAgent(w http.ResponseWriter, r *http.Request) {
if err := db.Query.DeleteAgentByID(context.Background(), r.PathValue("id")); err != nil {
http.Error(w, "Failed to delete agent", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
// GetAgentToken returns an agent token
func GetAgentToken(w http.ResponseWriter, r *http.Request) {
var data struct {
ServerURL string `json:"url"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if data.ServerURL == "" {
http.Error(w, "Server URL cannot be empty", http.StatusBadRequest)
return
}
token, err := util.EncodeAgentJWT(data.ServerURL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, map[string]string{"token": token})
}
// Profiles ------------------------------------------------------------------- // Profiles -------------------------------------------------------------------
// GetProfiles returns all profiles but without the dynamic data // GetProfiles returns all profiles but without the dynamic data

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/MizuchiLabs/mantrae/internal/db" "github.com/MizuchiLabs/mantrae/internal/db"
"github.com/MizuchiLabs/mantrae/pkg/util"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
@@ -152,7 +153,7 @@ func JWT(next http.HandlerFunc) http.HandlerFunc {
} }
// Validate JWT token // Validate JWT token
claims, err := ValidateJWT(token) claims, err := util.DecodeJWT(token)
if err != nil { if err != nil {
http.Error(w, "Unauthorized", http.StatusUnauthorized) http.Error(w, "Unauthorized", http.StatusUnauthorized)
return return

View File

@@ -47,7 +47,13 @@ func Routes(useAuth bool) http.Handler {
mux.HandleFunc("GET /api/settings/{key}", JWT(GetSetting)) mux.HandleFunc("GET /api/settings/{key}", JWT(GetSetting))
mux.HandleFunc("PUT /api/settings", JWT(UpdateSetting)) mux.HandleFunc("PUT /api/settings", JWT(UpdateSetting))
mux.HandleFunc("GET /api/agent", JWT(GetAgents))
mux.HandleFunc("GET /api/agent/{id}", JWT(GetAgent))
mux.HandleFunc("DELETE /api/agent", JWT(DeleteAgent))
mux.HandleFunc("GET /api/agent/token", JWT(GetAgentToken))
mux.HandleFunc("GET /api/ip/{id}", JWT(GetPublicIP)) mux.HandleFunc("GET /api/ip/{id}", JWT(GetPublicIP))
mux.HandleFunc("GET /api/backup", JWT(DownloadBackup)) mux.HandleFunc("GET /api/backup", JWT(DownloadBackup))
mux.HandleFunc("POST /api/restore", JWT(UploadBackup)) mux.HandleFunc("POST /api/restore", JWT(UploadBackup))

View File

@@ -24,6 +24,12 @@ type Flags struct {
UseAuth bool UseAuth bool
Update bool Update bool
Reset bool Reset bool
Agent AgentFlags
}
type AgentFlags struct {
Enabled bool
Port string
} }
func (f *Flags) Parse() error { func (f *Flags) Parse() error {
@@ -37,11 +43,15 @@ func (f *Flags) Parse() error {
) )
flag.StringVar(&f.Username, "username", "", "Specify the username for the Traefik instance") flag.StringVar(&f.Username, "username", "", "Specify the username for the Traefik instance")
flag.StringVar(&f.Password, "password", "", "Specify the password for the Traefik instance") flag.StringVar(&f.Password, "password", "", "Specify the password for the Traefik instance")
flag.StringVar(&f.Config, "config", "", "Specify the path to the database location") flag.StringVar(&f.Config, "config", "", "Specify the path to the config location")
flag.BoolVar(&f.UseAuth, "auth", false, "Use basic authentication for the profile endpoint") flag.BoolVar(&f.UseAuth, "auth", false, "Use basic authentication for the profile endpoint")
flag.BoolVar(&f.Update, "update", false, "Update the application") flag.BoolVar(&f.Update, "update", false, "Update the application")
flag.BoolVar(&f.Reset, "reset", false, "Reset the default admin password") flag.BoolVar(&f.Reset, "reset", false, "Reset the default admin password")
// Agent flags
flag.BoolVar(&f.Agent.Enabled, "agent.enabled", true, "Enable the agent server")
flag.StringVar(&f.Agent.Port, "agent.port", "8090", "Port to listen on for agents")
flag.Parse() flag.Parse()
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError) flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
@@ -49,6 +59,9 @@ func (f *Flags) Parse() error {
fmt.Println(util.Version) fmt.Println(util.Version)
os.Exit(0) os.Exit(0)
} }
if f.Config != "" {
util.MainDir = f.Config
}
if err := SetDefaultAdminUser(); err != nil { if err := SetDefaultAdminUser(); err != nil {
return err return err
} }
@@ -66,9 +79,6 @@ func (f *Flags) Parse() error {
return err return err
} }
} }
if f.Config != "" {
util.MainDir = f.Config
}
util.UpdateSelf(f.Update) util.UpdateSelf(f.Update)

View File

@@ -24,6 +24,9 @@ func New(db DBTX) *Queries {
func Prepare(ctx context.Context, db DBTX) (*Queries, error) { func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
q := Queries{db: db} q := Queries{db: db}
var err error var err error
if q.createAgentStmt, err = db.PrepareContext(ctx, createAgent); err != nil {
return nil, fmt.Errorf("error preparing query CreateAgent: %w", err)
}
if q.createConfigStmt, err = db.PrepareContext(ctx, createConfig); err != nil { if q.createConfigStmt, err = db.PrepareContext(ctx, createConfig); err != nil {
return nil, fmt.Errorf("error preparing query CreateConfig: %w", err) return nil, fmt.Errorf("error preparing query CreateConfig: %w", err)
} }
@@ -39,6 +42,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil { if q.createUserStmt, err = db.PrepareContext(ctx, createUser); err != nil {
return nil, fmt.Errorf("error preparing query CreateUser: %w", err) return nil, fmt.Errorf("error preparing query CreateUser: %w", err)
} }
if q.deleteAgentByHostnameStmt, err = db.PrepareContext(ctx, deleteAgentByHostname); err != nil {
return nil, fmt.Errorf("error preparing query DeleteAgentByHostname: %w", err)
}
if q.deleteAgentByIDStmt, err = db.PrepareContext(ctx, deleteAgentByID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteAgentByID: %w", err)
}
if q.deleteConfigByProfileIDStmt, err = db.PrepareContext(ctx, deleteConfigByProfileID); err != nil { if q.deleteConfigByProfileIDStmt, err = db.PrepareContext(ctx, deleteConfigByProfileID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteConfigByProfileID: %w", err) return nil, fmt.Errorf("error preparing query DeleteConfigByProfileID: %w", err)
} }
@@ -57,6 +66,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.deleteProviderByNameStmt, err = db.PrepareContext(ctx, deleteProviderByName); err != nil { if q.deleteProviderByNameStmt, err = db.PrepareContext(ctx, deleteProviderByName); err != nil {
return nil, fmt.Errorf("error preparing query DeleteProviderByName: %w", err) return nil, fmt.Errorf("error preparing query DeleteProviderByName: %w", err)
} }
if q.deleteSettingByIDStmt, err = db.PrepareContext(ctx, deleteSettingByID); err != nil {
return nil, fmt.Errorf("error preparing query DeleteSettingByID: %w", err)
}
if q.deleteSettingByKeyStmt, err = db.PrepareContext(ctx, deleteSettingByKey); err != nil { if q.deleteSettingByKeyStmt, err = db.PrepareContext(ctx, deleteSettingByKey); err != nil {
return nil, fmt.Errorf("error preparing query DeleteSettingByKey: %w", err) return nil, fmt.Errorf("error preparing query DeleteSettingByKey: %w", err)
} }
@@ -66,6 +78,12 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.deleteUserByUsernameStmt, err = db.PrepareContext(ctx, deleteUserByUsername); err != nil { if q.deleteUserByUsernameStmt, err = db.PrepareContext(ctx, deleteUserByUsername); err != nil {
return nil, fmt.Errorf("error preparing query DeleteUserByUsername: %w", err) return nil, fmt.Errorf("error preparing query DeleteUserByUsername: %w", err)
} }
if q.getAgentByHostnameStmt, err = db.PrepareContext(ctx, getAgentByHostname); err != nil {
return nil, fmt.Errorf("error preparing query GetAgentByHostname: %w", err)
}
if q.getAgentByIDStmt, err = db.PrepareContext(ctx, getAgentByID); err != nil {
return nil, fmt.Errorf("error preparing query GetAgentByID: %w", err)
}
if q.getConfigByProfileIDStmt, err = db.PrepareContext(ctx, getConfigByProfileID); err != nil { if q.getConfigByProfileIDStmt, err = db.PrepareContext(ctx, getConfigByProfileID); err != nil {
return nil, fmt.Errorf("error preparing query GetConfigByProfileID: %w", err) return nil, fmt.Errorf("error preparing query GetConfigByProfileID: %w", err)
} }
@@ -93,6 +111,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.getUserByUsernameStmt, err = db.PrepareContext(ctx, getUserByUsername); err != nil { if q.getUserByUsernameStmt, err = db.PrepareContext(ctx, getUserByUsername); err != nil {
return nil, fmt.Errorf("error preparing query GetUserByUsername: %w", err) return nil, fmt.Errorf("error preparing query GetUserByUsername: %w", err)
} }
if q.listAgentsStmt, err = db.PrepareContext(ctx, listAgents); err != nil {
return nil, fmt.Errorf("error preparing query ListAgents: %w", err)
}
if q.listConfigsStmt, err = db.PrepareContext(ctx, listConfigs); err != nil { if q.listConfigsStmt, err = db.PrepareContext(ctx, listConfigs); err != nil {
return nil, fmt.Errorf("error preparing query ListConfigs: %w", err) return nil, fmt.Errorf("error preparing query ListConfigs: %w", err)
} }
@@ -108,6 +129,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil { if q.listUsersStmt, err = db.PrepareContext(ctx, listUsers); err != nil {
return nil, fmt.Errorf("error preparing query ListUsers: %w", err) return nil, fmt.Errorf("error preparing query ListUsers: %w", err)
} }
if q.updateAgentStmt, err = db.PrepareContext(ctx, updateAgent); err != nil {
return nil, fmt.Errorf("error preparing query UpdateAgent: %w", err)
}
if q.updateConfigStmt, err = db.PrepareContext(ctx, updateConfig); err != nil { if q.updateConfigStmt, err = db.PrepareContext(ctx, updateConfig); err != nil {
return nil, fmt.Errorf("error preparing query UpdateConfig: %w", err) return nil, fmt.Errorf("error preparing query UpdateConfig: %w", err)
} }
@@ -123,6 +147,9 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
if q.updateUserStmt, err = db.PrepareContext(ctx, updateUser); err != nil { if q.updateUserStmt, err = db.PrepareContext(ctx, updateUser); err != nil {
return nil, fmt.Errorf("error preparing query UpdateUser: %w", err) return nil, fmt.Errorf("error preparing query UpdateUser: %w", err)
} }
if q.upsertAgentStmt, err = db.PrepareContext(ctx, upsertAgent); err != nil {
return nil, fmt.Errorf("error preparing query UpsertAgent: %w", err)
}
if q.upsertProfileStmt, err = db.PrepareContext(ctx, upsertProfile); err != nil { if q.upsertProfileStmt, err = db.PrepareContext(ctx, upsertProfile); err != nil {
return nil, fmt.Errorf("error preparing query UpsertProfile: %w", err) return nil, fmt.Errorf("error preparing query UpsertProfile: %w", err)
} }
@@ -140,6 +167,11 @@ func Prepare(ctx context.Context, db DBTX) (*Queries, error) {
func (q *Queries) Close() error { func (q *Queries) Close() error {
var err error var err error
if q.createAgentStmt != nil {
if cerr := q.createAgentStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createAgentStmt: %w", cerr)
}
}
if q.createConfigStmt != nil { if q.createConfigStmt != nil {
if cerr := q.createConfigStmt.Close(); cerr != nil { if cerr := q.createConfigStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing createConfigStmt: %w", cerr) err = fmt.Errorf("error closing createConfigStmt: %w", cerr)
@@ -165,6 +197,16 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing createUserStmt: %w", cerr) err = fmt.Errorf("error closing createUserStmt: %w", cerr)
} }
} }
if q.deleteAgentByHostnameStmt != nil {
if cerr := q.deleteAgentByHostnameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteAgentByHostnameStmt: %w", cerr)
}
}
if q.deleteAgentByIDStmt != nil {
if cerr := q.deleteAgentByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteAgentByIDStmt: %w", cerr)
}
}
if q.deleteConfigByProfileIDStmt != nil { if q.deleteConfigByProfileIDStmt != nil {
if cerr := q.deleteConfigByProfileIDStmt.Close(); cerr != nil { if cerr := q.deleteConfigByProfileIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteConfigByProfileIDStmt: %w", cerr) err = fmt.Errorf("error closing deleteConfigByProfileIDStmt: %w", cerr)
@@ -195,6 +237,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing deleteProviderByNameStmt: %w", cerr) err = fmt.Errorf("error closing deleteProviderByNameStmt: %w", cerr)
} }
} }
if q.deleteSettingByIDStmt != nil {
if cerr := q.deleteSettingByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteSettingByIDStmt: %w", cerr)
}
}
if q.deleteSettingByKeyStmt != nil { if q.deleteSettingByKeyStmt != nil {
if cerr := q.deleteSettingByKeyStmt.Close(); cerr != nil { if cerr := q.deleteSettingByKeyStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing deleteSettingByKeyStmt: %w", cerr) err = fmt.Errorf("error closing deleteSettingByKeyStmt: %w", cerr)
@@ -210,6 +257,16 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing deleteUserByUsernameStmt: %w", cerr) err = fmt.Errorf("error closing deleteUserByUsernameStmt: %w", cerr)
} }
} }
if q.getAgentByHostnameStmt != nil {
if cerr := q.getAgentByHostnameStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getAgentByHostnameStmt: %w", cerr)
}
}
if q.getAgentByIDStmt != nil {
if cerr := q.getAgentByIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getAgentByIDStmt: %w", cerr)
}
}
if q.getConfigByProfileIDStmt != nil { if q.getConfigByProfileIDStmt != nil {
if cerr := q.getConfigByProfileIDStmt.Close(); cerr != nil { if cerr := q.getConfigByProfileIDStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing getConfigByProfileIDStmt: %w", cerr) err = fmt.Errorf("error closing getConfigByProfileIDStmt: %w", cerr)
@@ -255,6 +312,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing getUserByUsernameStmt: %w", cerr) err = fmt.Errorf("error closing getUserByUsernameStmt: %w", cerr)
} }
} }
if q.listAgentsStmt != nil {
if cerr := q.listAgentsStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listAgentsStmt: %w", cerr)
}
}
if q.listConfigsStmt != nil { if q.listConfigsStmt != nil {
if cerr := q.listConfigsStmt.Close(); cerr != nil { if cerr := q.listConfigsStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing listConfigsStmt: %w", cerr) err = fmt.Errorf("error closing listConfigsStmt: %w", cerr)
@@ -280,6 +342,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing listUsersStmt: %w", cerr) err = fmt.Errorf("error closing listUsersStmt: %w", cerr)
} }
} }
if q.updateAgentStmt != nil {
if cerr := q.updateAgentStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateAgentStmt: %w", cerr)
}
}
if q.updateConfigStmt != nil { if q.updateConfigStmt != nil {
if cerr := q.updateConfigStmt.Close(); cerr != nil { if cerr := q.updateConfigStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing updateConfigStmt: %w", cerr) err = fmt.Errorf("error closing updateConfigStmt: %w", cerr)
@@ -305,6 +372,11 @@ func (q *Queries) Close() error {
err = fmt.Errorf("error closing updateUserStmt: %w", cerr) err = fmt.Errorf("error closing updateUserStmt: %w", cerr)
} }
} }
if q.upsertAgentStmt != nil {
if cerr := q.upsertAgentStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing upsertAgentStmt: %w", cerr)
}
}
if q.upsertProfileStmt != nil { if q.upsertProfileStmt != nil {
if cerr := q.upsertProfileStmt.Close(); cerr != nil { if cerr := q.upsertProfileStmt.Close(); cerr != nil {
err = fmt.Errorf("error closing upsertProfileStmt: %w", cerr) err = fmt.Errorf("error closing upsertProfileStmt: %w", cerr)
@@ -364,20 +436,26 @@ func (q *Queries) queryRow(ctx context.Context, stmt *sql.Stmt, query string, ar
type Queries struct { type Queries struct {
db DBTX db DBTX
tx *sql.Tx tx *sql.Tx
createAgentStmt *sql.Stmt
createConfigStmt *sql.Stmt createConfigStmt *sql.Stmt
createProfileStmt *sql.Stmt createProfileStmt *sql.Stmt
createProviderStmt *sql.Stmt createProviderStmt *sql.Stmt
createSettingStmt *sql.Stmt createSettingStmt *sql.Stmt
createUserStmt *sql.Stmt createUserStmt *sql.Stmt
deleteAgentByHostnameStmt *sql.Stmt
deleteAgentByIDStmt *sql.Stmt
deleteConfigByProfileIDStmt *sql.Stmt deleteConfigByProfileIDStmt *sql.Stmt
deleteConfigByProfileNameStmt *sql.Stmt deleteConfigByProfileNameStmt *sql.Stmt
deleteProfileByIDStmt *sql.Stmt deleteProfileByIDStmt *sql.Stmt
deleteProfileByNameStmt *sql.Stmt deleteProfileByNameStmt *sql.Stmt
deleteProviderByIDStmt *sql.Stmt deleteProviderByIDStmt *sql.Stmt
deleteProviderByNameStmt *sql.Stmt deleteProviderByNameStmt *sql.Stmt
deleteSettingByIDStmt *sql.Stmt
deleteSettingByKeyStmt *sql.Stmt deleteSettingByKeyStmt *sql.Stmt
deleteUserByIDStmt *sql.Stmt deleteUserByIDStmt *sql.Stmt
deleteUserByUsernameStmt *sql.Stmt deleteUserByUsernameStmt *sql.Stmt
getAgentByHostnameStmt *sql.Stmt
getAgentByIDStmt *sql.Stmt
getConfigByProfileIDStmt *sql.Stmt getConfigByProfileIDStmt *sql.Stmt
getConfigByProfileNameStmt *sql.Stmt getConfigByProfileNameStmt *sql.Stmt
getProfileByIDStmt *sql.Stmt getProfileByIDStmt *sql.Stmt
@@ -387,16 +465,19 @@ type Queries struct {
getSettingByKeyStmt *sql.Stmt getSettingByKeyStmt *sql.Stmt
getUserByIDStmt *sql.Stmt getUserByIDStmt *sql.Stmt
getUserByUsernameStmt *sql.Stmt getUserByUsernameStmt *sql.Stmt
listAgentsStmt *sql.Stmt
listConfigsStmt *sql.Stmt listConfigsStmt *sql.Stmt
listProfilesStmt *sql.Stmt listProfilesStmt *sql.Stmt
listProvidersStmt *sql.Stmt listProvidersStmt *sql.Stmt
listSettingsStmt *sql.Stmt listSettingsStmt *sql.Stmt
listUsersStmt *sql.Stmt listUsersStmt *sql.Stmt
updateAgentStmt *sql.Stmt
updateConfigStmt *sql.Stmt updateConfigStmt *sql.Stmt
updateProfileStmt *sql.Stmt updateProfileStmt *sql.Stmt
updateProviderStmt *sql.Stmt updateProviderStmt *sql.Stmt
updateSettingStmt *sql.Stmt updateSettingStmt *sql.Stmt
updateUserStmt *sql.Stmt updateUserStmt *sql.Stmt
upsertAgentStmt *sql.Stmt
upsertProfileStmt *sql.Stmt upsertProfileStmt *sql.Stmt
upsertProviderStmt *sql.Stmt upsertProviderStmt *sql.Stmt
upsertSettingStmt *sql.Stmt upsertSettingStmt *sql.Stmt
@@ -407,20 +488,26 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{ return &Queries{
db: tx, db: tx,
tx: tx, tx: tx,
createAgentStmt: q.createAgentStmt,
createConfigStmt: q.createConfigStmt, createConfigStmt: q.createConfigStmt,
createProfileStmt: q.createProfileStmt, createProfileStmt: q.createProfileStmt,
createProviderStmt: q.createProviderStmt, createProviderStmt: q.createProviderStmt,
createSettingStmt: q.createSettingStmt, createSettingStmt: q.createSettingStmt,
createUserStmt: q.createUserStmt, createUserStmt: q.createUserStmt,
deleteAgentByHostnameStmt: q.deleteAgentByHostnameStmt,
deleteAgentByIDStmt: q.deleteAgentByIDStmt,
deleteConfigByProfileIDStmt: q.deleteConfigByProfileIDStmt, deleteConfigByProfileIDStmt: q.deleteConfigByProfileIDStmt,
deleteConfigByProfileNameStmt: q.deleteConfigByProfileNameStmt, deleteConfigByProfileNameStmt: q.deleteConfigByProfileNameStmt,
deleteProfileByIDStmt: q.deleteProfileByIDStmt, deleteProfileByIDStmt: q.deleteProfileByIDStmt,
deleteProfileByNameStmt: q.deleteProfileByNameStmt, deleteProfileByNameStmt: q.deleteProfileByNameStmt,
deleteProviderByIDStmt: q.deleteProviderByIDStmt, deleteProviderByIDStmt: q.deleteProviderByIDStmt,
deleteProviderByNameStmt: q.deleteProviderByNameStmt, deleteProviderByNameStmt: q.deleteProviderByNameStmt,
deleteSettingByIDStmt: q.deleteSettingByIDStmt,
deleteSettingByKeyStmt: q.deleteSettingByKeyStmt, deleteSettingByKeyStmt: q.deleteSettingByKeyStmt,
deleteUserByIDStmt: q.deleteUserByIDStmt, deleteUserByIDStmt: q.deleteUserByIDStmt,
deleteUserByUsernameStmt: q.deleteUserByUsernameStmt, deleteUserByUsernameStmt: q.deleteUserByUsernameStmt,
getAgentByHostnameStmt: q.getAgentByHostnameStmt,
getAgentByIDStmt: q.getAgentByIDStmt,
getConfigByProfileIDStmt: q.getConfigByProfileIDStmt, getConfigByProfileIDStmt: q.getConfigByProfileIDStmt,
getConfigByProfileNameStmt: q.getConfigByProfileNameStmt, getConfigByProfileNameStmt: q.getConfigByProfileNameStmt,
getProfileByIDStmt: q.getProfileByIDStmt, getProfileByIDStmt: q.getProfileByIDStmt,
@@ -430,16 +517,19 @@ func (q *Queries) WithTx(tx *sql.Tx) *Queries {
getSettingByKeyStmt: q.getSettingByKeyStmt, getSettingByKeyStmt: q.getSettingByKeyStmt,
getUserByIDStmt: q.getUserByIDStmt, getUserByIDStmt: q.getUserByIDStmt,
getUserByUsernameStmt: q.getUserByUsernameStmt, getUserByUsernameStmt: q.getUserByUsernameStmt,
listAgentsStmt: q.listAgentsStmt,
listConfigsStmt: q.listConfigsStmt, listConfigsStmt: q.listConfigsStmt,
listProfilesStmt: q.listProfilesStmt, listProfilesStmt: q.listProfilesStmt,
listProvidersStmt: q.listProvidersStmt, listProvidersStmt: q.listProvidersStmt,
listSettingsStmt: q.listSettingsStmt, listSettingsStmt: q.listSettingsStmt,
listUsersStmt: q.listUsersStmt, listUsersStmt: q.listUsersStmt,
updateAgentStmt: q.updateAgentStmt,
updateConfigStmt: q.updateConfigStmt, updateConfigStmt: q.updateConfigStmt,
updateProfileStmt: q.updateProfileStmt, updateProfileStmt: q.updateProfileStmt,
updateProviderStmt: q.updateProviderStmt, updateProviderStmt: q.updateProviderStmt,
updateSettingStmt: q.updateSettingStmt, updateSettingStmt: q.updateSettingStmt,
updateUserStmt: q.updateUserStmt, updateUserStmt: q.updateUserStmt,
upsertAgentStmt: q.upsertAgentStmt,
upsertProfileStmt: q.upsertProfileStmt, upsertProfileStmt: q.upsertProfileStmt,
upsertProviderStmt: q.upsertProviderStmt, upsertProviderStmt: q.upsertProviderStmt,
upsertSettingStmt: q.upsertSettingStmt, upsertSettingStmt: q.upsertSettingStmt,

View File

@@ -45,6 +45,15 @@ CREATE TABLE settings (
value TEXT NOT NULL value TEXT NOT NULL
); );
CREATE TABLE agents (
id TEXT PRIMARY KEY,
hostname VARCHAR(100) NOT NULL,
public_ip TEXT,
private_ips JSONB,
containers JSONB,
last_seen DATETIME
);
-- +goose StatementBegin -- +goose StatementBegin
CREATE TRIGGER add_profile_config AFTER INSERT ON profiles FOR EACH ROW BEGIN CREATE TRIGGER add_profile_config AFTER INSERT ON profiles FOR EACH ROW BEGIN
INSERT INTO INSERT INTO
@@ -89,6 +98,10 @@ DROP TABLE users;
DROP TABLE settings; DROP TABLE settings;
DROP TABLE agents;
DROP TABLE containers;
DROP TRIGGER add_profile_config; DROP TRIGGER add_profile_config;
DROP TRIGGER ensure_single_active_insert; DROP TRIGGER ensure_single_active_insert;

View File

@@ -4,6 +4,19 @@
package db package db
import (
"time"
)
type Agent struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
PublicIp *string `json:"public_ip"`
PrivateIps interface{} `json:"private_ips"`
Containers interface{} `json:"containers"`
LastSeen *time.Time `json:"last_seen"`
}
type Config struct { type Config struct {
ProfileID int64 `json:"profile_id"` ProfileID int64 `json:"profile_id"`
Overview interface{} `json:"overview"` Overview interface{} `json:"overview"`

View File

@@ -9,20 +9,26 @@ import (
) )
type Querier interface { type Querier interface {
CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent, error)
CreateConfig(ctx context.Context, arg CreateConfigParams) (Config, error) CreateConfig(ctx context.Context, arg CreateConfigParams) (Config, error)
CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error) CreateProfile(ctx context.Context, arg CreateProfileParams) (Profile, error)
CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error) CreateProvider(ctx context.Context, arg CreateProviderParams) (Provider, error)
CreateSetting(ctx context.Context, arg CreateSettingParams) (Setting, error) CreateSetting(ctx context.Context, arg CreateSettingParams) (Setting, error)
CreateUser(ctx context.Context, arg CreateUserParams) (User, error) CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
DeleteAgentByHostname(ctx context.Context, hostname string) error
DeleteAgentByID(ctx context.Context, id string) error
DeleteConfigByProfileID(ctx context.Context, profileID int64) error DeleteConfigByProfileID(ctx context.Context, profileID int64) error
DeleteConfigByProfileName(ctx context.Context, name string) error DeleteConfigByProfileName(ctx context.Context, name string) error
DeleteProfileByID(ctx context.Context, id int64) error DeleteProfileByID(ctx context.Context, id int64) error
DeleteProfileByName(ctx context.Context, name string) error DeleteProfileByName(ctx context.Context, name string) error
DeleteProviderByID(ctx context.Context, id int64) error DeleteProviderByID(ctx context.Context, id int64) error
DeleteProviderByName(ctx context.Context, name string) error DeleteProviderByName(ctx context.Context, name string) error
DeleteSettingByID(ctx context.Context, id int64) error
DeleteSettingByKey(ctx context.Context, key string) error DeleteSettingByKey(ctx context.Context, key string) error
DeleteUserByID(ctx context.Context, id int64) error DeleteUserByID(ctx context.Context, id int64) error
DeleteUserByUsername(ctx context.Context, username string) error DeleteUserByUsername(ctx context.Context, username string) error
GetAgentByHostname(ctx context.Context, hostname string) (Agent, error)
GetAgentByID(ctx context.Context, id string) (Agent, error)
GetConfigByProfileID(ctx context.Context, profileID int64) (Config, error) GetConfigByProfileID(ctx context.Context, profileID int64) (Config, error)
GetConfigByProfileName(ctx context.Context, name string) (Config, error) GetConfigByProfileName(ctx context.Context, name string) (Config, error)
GetProfileByID(ctx context.Context, id int64) (Profile, error) GetProfileByID(ctx context.Context, id int64) (Profile, error)
@@ -32,16 +38,19 @@ type Querier interface {
GetSettingByKey(ctx context.Context, key string) (Setting, error) GetSettingByKey(ctx context.Context, key string) (Setting, error)
GetUserByID(ctx context.Context, id int64) (User, error) GetUserByID(ctx context.Context, id int64) (User, error)
GetUserByUsername(ctx context.Context, username string) (User, error) GetUserByUsername(ctx context.Context, username string) (User, error)
ListAgents(ctx context.Context) ([]Agent, error)
ListConfigs(ctx context.Context) ([]Config, error) ListConfigs(ctx context.Context) ([]Config, error)
ListProfiles(ctx context.Context) ([]Profile, error) ListProfiles(ctx context.Context) ([]Profile, error)
ListProviders(ctx context.Context) ([]Provider, error) ListProviders(ctx context.Context) ([]Provider, error)
ListSettings(ctx context.Context) ([]Setting, error) ListSettings(ctx context.Context) ([]Setting, error)
ListUsers(ctx context.Context) ([]User, error) ListUsers(ctx context.Context) ([]User, error)
UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent, error)
UpdateConfig(ctx context.Context, arg UpdateConfigParams) (Config, error) UpdateConfig(ctx context.Context, arg UpdateConfigParams) (Config, error)
UpdateProfile(ctx context.Context, arg UpdateProfileParams) (Profile, error) UpdateProfile(ctx context.Context, arg UpdateProfileParams) (Profile, error)
UpdateProvider(ctx context.Context, arg UpdateProviderParams) (Provider, error) UpdateProvider(ctx context.Context, arg UpdateProviderParams) (Provider, error)
UpdateSetting(ctx context.Context, arg UpdateSettingParams) (Setting, error) UpdateSetting(ctx context.Context, arg UpdateSettingParams) (Setting, error)
UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, error)
UpsertAgent(ctx context.Context, arg UpsertAgentParams) (Agent, error)
UpsertProfile(ctx context.Context, arg UpsertProfileParams) (Profile, error) UpsertProfile(ctx context.Context, arg UpsertProfileParams) (Profile, error)
UpsertProvider(ctx context.Context, arg UpsertProviderParams) (Provider, error) UpsertProvider(ctx context.Context, arg UpsertProviderParams) (Provider, error)
UpsertSetting(ctx context.Context, arg UpsertSettingParams) (Setting, error) UpsertSetting(ctx context.Context, arg UpsertSettingParams) (Setting, error)

View File

@@ -332,7 +332,92 @@ SET
key = EXCLUDED.key, key = EXCLUDED.key,
value = EXCLUDED.value RETURNING *; value = EXCLUDED.value RETURNING *;
-- name: DeleteSettingByID :exec
DELETE FROM settings
WHERE
id = ?;
-- name: DeleteSettingByKey :exec -- name: DeleteSettingByKey :exec
DELETE FROM settings DELETE FROM settings
WHERE WHERE
key = ?; key = ?;
-- name: GetAgentByID :one
SELECT
*
FROM
agents
WHERE
id = ?
LIMIT
1;
-- name: GetAgentByHostname :one
SELECT
*
FROM
agents
WHERE
hostname = ?
LIMIT
1;
-- name: ListAgents :many
SELECT
*
FROM
agents;
-- name: CreateAgent :one
INSERT INTO
agents (
id,
hostname,
public_ip,
private_ips,
containers,
last_seen
)
VALUES
(?, ?, ?, ?, ?, ?) RETURNING *;
-- name: UpdateAgent :one
UPDATE agents
SET
hostname = ?,
public_ip = ?,
private_ips = ?,
containers = ?,
last_seen = ?
WHERE
id = ? RETURNING *;
-- name: UpsertAgent :one
INSERT INTO
agents (
id,
hostname,
public_ip,
private_ips,
containers,
last_seen
)
VALUES
(?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO
UPDATE
SET
hostname = EXCLUDED.hostname,
public_ip = EXCLUDED.public_ip,
private_ips = EXCLUDED.private_ips,
containers = EXCLUDED.containers,
last_seen = EXCLUDED.last_seen RETURNING *;
-- name: DeleteAgentByID :exec
DELETE FROM agents
WHERE
id = ?;
-- name: DeleteAgentByHostname :exec
DELETE FROM agents
WHERE
hostname = ?;

View File

@@ -7,8 +7,53 @@ package db
import ( import (
"context" "context"
"time"
) )
const createAgent = `-- name: CreateAgent :one
INSERT INTO
agents (
id,
hostname,
public_ip,
private_ips,
containers,
last_seen
)
VALUES
(?, ?, ?, ?, ?, ?) RETURNING id, hostname, public_ip, private_ips, containers, last_seen
`
type CreateAgentParams struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
PublicIp *string `json:"public_ip"`
PrivateIps interface{} `json:"private_ips"`
Containers interface{} `json:"containers"`
LastSeen *time.Time `json:"last_seen"`
}
func (q *Queries) CreateAgent(ctx context.Context, arg CreateAgentParams) (Agent, error) {
row := q.queryRow(ctx, q.createAgentStmt, createAgent,
arg.ID,
arg.Hostname,
arg.PublicIp,
arg.PrivateIps,
arg.Containers,
arg.LastSeen,
)
var i Agent
err := row.Scan(
&i.ID,
&i.Hostname,
&i.PublicIp,
&i.PrivateIps,
&i.Containers,
&i.LastSeen,
)
return i, err
}
const createConfig = `-- name: CreateConfig :one const createConfig = `-- name: CreateConfig :one
INSERT INTO INSERT INTO
config ( config (
@@ -196,6 +241,28 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
return i, err return i, err
} }
const deleteAgentByHostname = `-- name: DeleteAgentByHostname :exec
DELETE FROM agents
WHERE
hostname = ?
`
func (q *Queries) DeleteAgentByHostname(ctx context.Context, hostname string) error {
_, err := q.exec(ctx, q.deleteAgentByHostnameStmt, deleteAgentByHostname, hostname)
return err
}
const deleteAgentByID = `-- name: DeleteAgentByID :exec
DELETE FROM agents
WHERE
id = ?
`
func (q *Queries) DeleteAgentByID(ctx context.Context, id string) error {
_, err := q.exec(ctx, q.deleteAgentByIDStmt, deleteAgentByID, id)
return err
}
const deleteConfigByProfileID = `-- name: DeleteConfigByProfileID :exec const deleteConfigByProfileID = `-- name: DeleteConfigByProfileID :exec
DELETE FROM config DELETE FROM config
WHERE WHERE
@@ -269,6 +336,17 @@ func (q *Queries) DeleteProviderByName(ctx context.Context, name string) error {
return err return err
} }
const deleteSettingByID = `-- name: DeleteSettingByID :exec
DELETE FROM settings
WHERE
id = ?
`
func (q *Queries) DeleteSettingByID(ctx context.Context, id int64) error {
_, err := q.exec(ctx, q.deleteSettingByIDStmt, deleteSettingByID, id)
return err
}
const deleteSettingByKey = `-- name: DeleteSettingByKey :exec const deleteSettingByKey = `-- name: DeleteSettingByKey :exec
DELETE FROM settings DELETE FROM settings
WHERE WHERE
@@ -302,6 +380,56 @@ func (q *Queries) DeleteUserByUsername(ctx context.Context, username string) err
return err return err
} }
const getAgentByHostname = `-- name: GetAgentByHostname :one
SELECT
id, hostname, public_ip, private_ips, containers, last_seen
FROM
agents
WHERE
hostname = ?
LIMIT
1
`
func (q *Queries) GetAgentByHostname(ctx context.Context, hostname string) (Agent, error) {
row := q.queryRow(ctx, q.getAgentByHostnameStmt, getAgentByHostname, hostname)
var i Agent
err := row.Scan(
&i.ID,
&i.Hostname,
&i.PublicIp,
&i.PrivateIps,
&i.Containers,
&i.LastSeen,
)
return i, err
}
const getAgentByID = `-- name: GetAgentByID :one
SELECT
id, hostname, public_ip, private_ips, containers, last_seen
FROM
agents
WHERE
id = ?
LIMIT
1
`
func (q *Queries) GetAgentByID(ctx context.Context, id string) (Agent, error) {
row := q.queryRow(ctx, q.getAgentByIDStmt, getAgentByID, id)
var i Agent
err := row.Scan(
&i.ID,
&i.Hostname,
&i.PublicIp,
&i.PrivateIps,
&i.Containers,
&i.LastSeen,
)
return i, err
}
const getConfigByProfileID = `-- name: GetConfigByProfileID :one const getConfigByProfileID = `-- name: GetConfigByProfileID :one
SELECT SELECT
profile_id, overview, entrypoints, routers, services, middlewares, tls, version profile_id, overview, entrypoints, routers, services, middlewares, tls, version
@@ -533,6 +661,43 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
return i, err return i, err
} }
const listAgents = `-- name: ListAgents :many
SELECT
id, hostname, public_ip, private_ips, containers, last_seen
FROM
agents
`
func (q *Queries) ListAgents(ctx context.Context) ([]Agent, error) {
rows, err := q.query(ctx, q.listAgentsStmt, listAgents)
if err != nil {
return nil, err
}
defer rows.Close()
var items []Agent
for rows.Next() {
var i Agent
if err := rows.Scan(
&i.ID,
&i.Hostname,
&i.PublicIp,
&i.PrivateIps,
&i.Containers,
&i.LastSeen,
); 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 listConfigs = `-- name: ListConfigs :many const listConfigs = `-- name: ListConfigs :many
SELECT SELECT
profile_id, overview, entrypoints, routers, services, middlewares, tls, version profile_id, overview, entrypoints, routers, services, middlewares, tls, version
@@ -714,6 +879,48 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) {
return items, nil return items, nil
} }
const updateAgent = `-- name: UpdateAgent :one
UPDATE agents
SET
hostname = ?,
public_ip = ?,
private_ips = ?,
containers = ?,
last_seen = ?
WHERE
id = ? RETURNING id, hostname, public_ip, private_ips, containers, last_seen
`
type UpdateAgentParams struct {
Hostname string `json:"hostname"`
PublicIp *string `json:"public_ip"`
PrivateIps interface{} `json:"private_ips"`
Containers interface{} `json:"containers"`
LastSeen *time.Time `json:"last_seen"`
ID string `json:"id"`
}
func (q *Queries) UpdateAgent(ctx context.Context, arg UpdateAgentParams) (Agent, error) {
row := q.queryRow(ctx, q.updateAgentStmt, updateAgent,
arg.Hostname,
arg.PublicIp,
arg.PrivateIps,
arg.Containers,
arg.LastSeen,
arg.ID,
)
var i Agent
err := row.Scan(
&i.ID,
&i.Hostname,
&i.PublicIp,
&i.PrivateIps,
&i.Containers,
&i.LastSeen,
)
return i, err
}
const updateConfig = `-- name: UpdateConfig :one const updateConfig = `-- name: UpdateConfig :one
UPDATE config UPDATE config
SET SET
@@ -914,6 +1121,57 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, e
return i, err return i, err
} }
const upsertAgent = `-- name: UpsertAgent :one
INSERT INTO
agents (
id,
hostname,
public_ip,
private_ips,
containers,
last_seen
)
VALUES
(?, ?, ?, ?, ?, ?) ON CONFLICT (id) DO
UPDATE
SET
hostname = EXCLUDED.hostname,
public_ip = EXCLUDED.public_ip,
private_ips = EXCLUDED.private_ips,
containers = EXCLUDED.containers,
last_seen = EXCLUDED.last_seen RETURNING id, hostname, public_ip, private_ips, containers, last_seen
`
type UpsertAgentParams struct {
ID string `json:"id"`
Hostname string `json:"hostname"`
PublicIp *string `json:"public_ip"`
PrivateIps interface{} `json:"private_ips"`
Containers interface{} `json:"containers"`
LastSeen *time.Time `json:"last_seen"`
}
func (q *Queries) UpsertAgent(ctx context.Context, arg UpsertAgentParams) (Agent, error) {
row := q.queryRow(ctx, q.upsertAgentStmt, upsertAgent,
arg.ID,
arg.Hostname,
arg.PublicIp,
arg.PrivateIps,
arg.Containers,
arg.LastSeen,
)
var i Agent
err := row.Scan(
&i.ID,
&i.Hostname,
&i.PublicIp,
&i.PrivateIps,
&i.Containers,
&i.LastSeen,
)
return i, err
}
const upsertProfile = `-- name: UpsertProfile :one const upsertProfile = `-- name: UpsertProfile :one
INSERT INTO INSERT INTO
profiles (id, name, url, username, password, tls) profiles (id, name, url, username, password, tls)

View File

@@ -54,6 +54,10 @@ func main() {
go traefik.Sync(ctx) go traefik.Sync(ctx)
go dns.Sync(ctx) go dns.Sync(ctx)
// Start the grpc server
go api.Server(flags.Agent.Port)
// Start the WebUI server
srv := &http.Server{ srv := &http.Server{
Addr: ":" + flags.Port, Addr: ":" + flags.Port,
Handler: api.Routes(flags.UseAuth), Handler: api.Routes(flags.UseAuth),

View File

@@ -7,7 +7,10 @@ import (
"log/slog" "log/slog"
"net" "net"
"net/http" "net/http"
"strings"
"time" "time"
"golang.org/x/exp/slices"
) )
// Public IP APIs // Public IP APIs
@@ -69,3 +72,40 @@ func GetPublicIP() (string, error) {
} }
return "", fmt.Errorf("failed to get public IP") return "", fmt.Errorf("failed to get public IP")
} }
func GetPrivateIP() ([]string, error) {
var ips []string
interfaces, err := net.Interfaces()
if err != nil {
return nil, err
}
excluded := []string{"lo", "docker", "br-", "veth", "kube", "cni"}
for _, iface := range interfaces {
if slices.ContainsFunc(excluded, func(s string) bool {
return strings.Contains(iface.Name, s)
}) || iface.Flags&net.FlagUp == 0 {
continue
} else {
addrs, err := iface.Addrs()
if err != nil {
return nil, err
}
for _, addr := range addrs {
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ips = append(ips, ipnet.IP.String())
}
}
}
}
}
if len(ips) == 0 {
return nil, errors.New("no private IP addresses found")
}
return ips, nil
}

View File

@@ -4,16 +4,26 @@ package util
import ( import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors"
"net" "net"
"net/url" "net/url"
"os" "os"
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/golang-jwt/jwt/v5"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
type Claims struct {
Username string `json:"username,omitempty"`
ServerURL string `json:"server_url,omitempty"`
Secret string `json:"secret,omitempty"`
jwt.RegisteredClaims
}
// GenPassword generates a random password of the specified length // GenPassword generates a random password of the specified length
func GenPassword(length int) string { func GenPassword(length int) string {
bytes := make([]byte, length) bytes := make([]byte, length)
@@ -53,6 +63,73 @@ func HashBasicAuth(userString string) (string, error) {
return user + ":" + string(hash), nil return user + ":" + string(hash), nil
} }
// EncodeUserJWT generates a JWT for user login
func EncodeUserJWT(username string) (string, error) {
secret := os.Getenv("SECRET")
if secret == "" {
return "", errors.New("SECRET environment variable is not set")
}
if username == "" {
return "", errors.New("username cannot be empty")
}
expirationTime := time.Now().Add(24 * time.Hour)
claims := &Claims{
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret))
}
// EncodeAgentJWT generates a JWT for the agent
func EncodeAgentJWT(serverURL string) (string, error) {
secret := os.Getenv("SECRET")
if secret == "" {
return "", errors.New("SECRET environment variable is not set")
}
if serverURL == "" {
return "", errors.New("serverURL cannot be empty")
}
expirationTime := time.Now().Add(14 * 24 * time.Hour) // 14 days
claims := Claims{
ServerURL: serverURL,
Secret: secret, // Optionally store the secret here
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(secret)) // Server uses its secret for signing
}
// DecodeJWT decodes the token and returns claims if valid
func DecodeJWT(tokenString string) (*Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(
tokenString,
claims,
func(token *jwt.Token) (interface{}, error) {
// Validate the algorithm and return the server's secret
return []byte(os.Getenv("SECRET")), nil // Use the secret from claims to verify
},
)
if err != nil || !token.Valid {
return nil, err
}
return claims, nil
}
// IsValidURL checks if a URL is valid url string // IsValidURL checks if a URL is valid url string
func IsValidURL(u string) bool { func IsValidURL(u string) bool {
// If no scheme is provided, prepend "http://" // If no scheme is provided, prepend "http://"