From 45495f4b47d8f5ea6d4948b577653d77c888b8ee Mon Sep 17 00:00:00 2001 From: Steven Tracey Date: Wed, 4 Mar 2026 18:28:52 +0100 Subject: [PATCH] Major refactoring --- Makefile | 10 +- certman-exec.service | 15 +++ certman.socket | 12 ++ client/client.go | 2 +- client/grpc.go | 57 ++++++++ commands/executor.go | 27 ++++ commands/install.go | 22 ++- executor/executor.go | 43 ++++++ executor/hook.go | 73 ++++++++++ executor/util.go | 8 ++ go.mod | 4 + go.sum | 35 ++++- internal/config.go | 21 ++- internal/grpc.go | 1 + internal/util.go | 70 +++++++++- main.go | 8 ++ proto/hook.proto | 27 ++++ proto/symlink.proto | 5 + proto/v1/hook.pb.go | 280 +++++++++++++++++++++++++++++++++++++++ proto/v1/hook_grpc.pb.go | 121 +++++++++++++++++ proto/v1/symlink.pb.go | 59 +++++++++ 21 files changed, 885 insertions(+), 15 deletions(-) create mode 100644 certman-exec.service create mode 100644 certman.socket create mode 100644 client/grpc.go create mode 100644 commands/executor.go create mode 100644 executor/executor.go create mode 100644 executor/hook.go create mode 100644 executor/util.go create mode 100644 internal/grpc.go create mode 100644 proto/hook.proto create mode 100644 proto/symlink.proto create mode 100644 proto/v1/hook.pb.go create mode 100644 proto/v1/hook_grpc.pb.go create mode 100644 proto/v1/symlink.pb.go diff --git a/Makefile b/Makefile index bcc5def..2c5e7a4 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION := 1.0.0-beta +VERSION := 1.0.1-beta BUILD := $(shell git rev-parse --short HEAD) GO := go @@ -6,7 +6,13 @@ GO := go BUILD_FLAGS := -buildmode=pie -trimpath LDFLAGS := -linkmode=external -extldflags="-Wl,-z,relro,-z,now" -X git.nevets.tech/Keys/CertManager/internal.Version=$(VERSION) -X git.nevets.tech/Keys/CertManager/internal.Build=$(BUILD) -build: +.PHONY: proto build stage + +proto: + @protoc --go_out=./proto --go-grpc_out=./proto proto/hook.proto + @protoc --go_out=./proto --go-grpc_out=./proto proto/symlink.proto + +build: proto $(GO) build $(BUILD_FLAGS) -ldflags="$(LDFLAGS)" -o ./certman . @cp ./certman ./certman-$(VERSION)-amd64 diff --git a/certman-exec.service b/certman-exec.service new file mode 100644 index 0000000..b323b8f --- /dev/null +++ b/certman-exec.service @@ -0,0 +1,15 @@ +[Unit] +Description=CertMan Executor daemon +Requires=certman.socket +After=network.target + +[Service] +ExecStart=/usr/local/bin/certman executor +User=root +Group=root + +KillSignal=SIGTERM +TimeoutStopSec=30 + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/certman.socket b/certman.socket new file mode 100644 index 0000000..57d8c71 --- /dev/null +++ b/certman.socket @@ -0,0 +1,12 @@ +[Unit] +Description=certman hook daemon socket + +[Socket] +ListenStream=/run/certman.sock +SocketUser=root +SocketGroup=certsock +SocketMode=0660 +RemoveOnStop=true + +[Install] +WantedBy=sockets.target \ No newline at end of file diff --git a/client/client.go b/client/client.go index b67c0a4..08775f2 100644 --- a/client/client.go +++ b/client/client.go @@ -135,7 +135,7 @@ func Tick() { keyLinks := domainConfig.GetStringSlice("Certificates.key_symlinks") for _, keyLink := range keyLinks { - err = internal.LinkFile(filepath.Join(certsDir, domainStr+".crt"), keyLink, domainStr, ".key") + err = internal.LinkFile(filepath.Join(certsDir, domainStr+".key"), keyLink, domainStr, ".key") if err != nil { fmt.Printf("Error linking cert %s to %s: %v\n", keyLink, domainStr, err) continue diff --git a/client/grpc.go b/client/grpc.go new file mode 100644 index 0000000..0e32b3d --- /dev/null +++ b/client/grpc.go @@ -0,0 +1,57 @@ +package client + +import ( + "context" + "flag" + "fmt" + "log" + "time" + + "git.nevets.tech/Keys/CertManager/internal" + pb "git.nevets.tech/Keys/CertManager/proto/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" +) + +var ( + tls = flag.Bool("tls", false, "Connection uses TLS if true, else plain TCP") + caFile = flag.String("ca_file", "", "The file containing the CA root cert file") + serverAddr = flag.String("addr", "localhost:50051", "The server address in the format of host:port") + serverHostOverride = flag.String("server_host_override", "x.test.example.com", "The server name used to verify the hostname returned by the TLS handshake") +) + +func SendHook(domain string) { + conn, err := grpc.NewClient( + "unix:///run/certman.sock", + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) + if err != nil { + log.Fatalf("fail to dial: %v", err) + } + defer conn.Close() + client := pb.NewHookServiceClient(conn) + + hooks, err := internal.PostPullHooks(domain) + if err != nil { + fmt.Printf("Error getting hooks: %v\n", err) + return + } + + for _, hook := range hooks { + sendHook(client, hook) + } +} + +func sendHook(client pb.HookServiceClient, hook *pb.Hook) { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + res, err := client.ExecuteHook(ctx, &pb.ExecuteHookRequest{Hook: hook}) + if err != nil { + fmt.Printf("Error executing hook: %v\n", err) + return + } + + if res.GetError() != "" { + fmt.Printf("Error executing hook: %s\n", res.GetError()) + } +} diff --git a/commands/executor.go b/commands/executor.go new file mode 100644 index 0000000..a8dffb3 --- /dev/null +++ b/commands/executor.go @@ -0,0 +1,27 @@ +package commands + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "git.nevets.tech/Keys/CertManager/executor" +) + +var executorServer *executor.Server + +func StartExecutorCmd() error { + executorServer = &executor.Server{} + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + executorServer.Stop() + }() + if err := executorServer.Start(); err != nil { + return fmt.Errorf("failed to start executor server: %w", err) + } + return nil +} diff --git a/commands/install.go b/commands/install.go index 69f4c62..7159f18 100644 --- a/commands/install.go +++ b/commands/install.go @@ -13,8 +13,13 @@ import ( func NewDomainCmd(domain, domainDir string, dirOverridden bool) error { //TODO add config option for "overriden dir" + err := internal.LoadConfig() + if err != nil { + return err + } + fmt.Printf("Creating new domain %s\n", domain) - err := internal.CreateDomainConfig(domain) + err = internal.CreateDomainConfig(domain) if err != nil { return err } @@ -54,6 +59,11 @@ func InstallCmd(isThin bool, mode string) error { internal.MakeDirs() internal.CreateConfig(mode) + err := internal.LoadConfig() + if err != nil { + return err + } + f, err := os.OpenFile("/var/run/certman.pid", os.O_RDONLY|os.O_CREATE, 0755) if err != nil { return fmt.Errorf("error creating pid file: %v", err) @@ -65,7 +75,15 @@ func InstallCmd(isThin bool, mode string) error { newUserCmd := exec.Command("useradd", "-d", "/var/local/certman", "-U", "-r", "-s", "/sbin/nologin", "certman") if output, err := newUserCmd.CombinedOutput(); err != nil { - return fmt.Errorf("error creating user: %v: output %s", err, output) + if !strings.Contains(err.Error(), "exit status 9") { + return fmt.Errorf("error creating user: %v: output %s", err, output) + } + } + newGroupCmd := exec.Command("groupadd", "-r", "-U", "certman", "certsock") + if output, err := newGroupCmd.CombinedOutput(); err != nil { + if !strings.Contains(err.Error(), "exit status 9") { + return fmt.Errorf("error creating group: %v: output %s", err, output) + } } certmanUser, err := user.Lookup("certman") if err != nil { diff --git a/executor/executor.go b/executor/executor.go new file mode 100644 index 0000000..73ec903 --- /dev/null +++ b/executor/executor.go @@ -0,0 +1,43 @@ +package executor + +import ( + "fmt" + "net" + "sync" + + pb "git.nevets.tech/Keys/CertManager/proto/v1" + "github.com/coreos/go-systemd/v22/activation" + "google.golang.org/grpc" +) + +type Server struct { + listener net.Listener + wg sync.WaitGroup +} + +func (s *Server) Start() error { + listeners, err := activation.Listeners() + if err != nil { + return fmt.Errorf("systemd activation listeners: %v", err) + } + if len(listeners) != 1 { + return fmt.Errorf("systemd activation listeners: expected 1, got %d", len(listeners)) + } + + s.listener = listeners[0] + srv := grpc.NewServer() + pb.RegisterHookServiceServer(srv, &hookServer{}) + + err = srv.Serve(s.listener) + if err != nil { + return fmt.Errorf("error creating grpc listener: %v", err) + } + fmt.Printf("Started gRPC server on %s\n", s.listener.Addr()) + return nil +} + +func (s *Server) Stop() { + if s.listener != nil { + _ = s.listener.Close() + } +} diff --git a/executor/hook.go b/executor/hook.go new file mode 100644 index 0000000..318620d --- /dev/null +++ b/executor/hook.go @@ -0,0 +1,73 @@ +package executor + +import ( + "context" + "errors" + "os" + "os/exec" + "syscall" + "time" + + "git.nevets.tech/Keys/CertManager/internal" + pb "git.nevets.tech/Keys/CertManager/proto/v1" +) + +type hookServer struct { + pb.UnimplementedHookServiceServer +} + +func (s *hookServer) ExecuteHook(ctx context.Context, req *pb.ExecuteHookRequest) (*pb.ExecuteHookResponse, error) { + h := req.GetHook() + if h == nil { + return &pb.ExecuteHookResponse{Error: "missing hook"}, nil + } + + // Minimal validation + if len(h.GetCommand()) == 0 { + return &pb.ExecuteHookResponse{Error: "command is empty"}, nil + } + + // Timeout + timeout := time.Duration(h.GetTimeoutSeconds()) * time.Second + if timeout <= 0 { + timeout = 30 * time.Second + } + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + // Build command + cmdArgs := h.GetCommand() + cmd := exec.CommandContext(ctx, cmdArgs[0], cmdArgs[1:]...) + if cwd := h.GetCwd(); cwd != "" { + cmd.Dir = cwd + } + + // Env: inherit current + overlay provided + env := os.Environ() + for k, v := range h.GetEnv() { + env = append(env, k+"="+v) + } + cmd.Env = env + + // Run as user/group if specified (Linux/Unix) + if h.GetUser() != "" || h.GetGroup() != "" { + cred, err := internal.MakeCredential(h.GetUser(), h.GetGroup()) + if err != nil { + return &pb.ExecuteHookResponse{Error: brief(err)}, nil + } + cmd.SysProcAttr = &syscall.SysProcAttr{ + Credential: cred, + } + } + + // We’re intentionally NOT returning stdout/stderr; only a brief error on failure. + if err := cmd.Run(); err != nil { + // If context deadline hit, make the error message short and explicit. + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return &pb.ExecuteHookResponse{Error: "hook timed out"}, nil + } + return &pb.ExecuteHookResponse{Error: brief(err)}, nil + } + + return &pb.ExecuteHookResponse{Error: ""}, nil +} diff --git a/executor/util.go b/executor/util.go new file mode 100644 index 0000000..eb30ea8 --- /dev/null +++ b/executor/util.go @@ -0,0 +1,8 @@ +package executor + +import "fmt" + +// brief tries to keep errors short and non-leaky. +func brief(err error) string { + return fmt.Sprintf("hook failed: %v", err) +} diff --git a/go.mod b/go.mod index 30139fb..bfbc262 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.25.0 require ( code.gitea.io/sdk/gitea v0.23.2 + github.com/coreos/go-systemd/v22 v22.7.0 github.com/go-acme/lego/v4 v4.32.0 github.com/go-git/go-billy/v5 v5.8.0 github.com/go-git/go-git/v5 v5.17.0 @@ -11,6 +12,8 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 golang.org/x/crypto v0.48.0 + google.golang.org/grpc v1.78.0 + google.golang.org/protobuf v1.36.11 ) require ( @@ -52,5 +55,6 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect golang.org/x/tools v0.42.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect ) diff --git a/go.sum b/go.sum index 829c7e4..890e900 100644 --- a/go.sum +++ b/go.sum @@ -15,8 +15,12 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= @@ -50,17 +54,18 @@ github.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxe github.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg= -github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA= -github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0= -github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= @@ -123,6 +128,18 @@ github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8 github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= +go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= +go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= +go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -163,6 +180,14 @@ golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20 h1:Jr5R2J6F6qWyzINc+4AM8t5pfUz6beZpHp678GNrMbE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260203192932-546029d2fa20/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/config.go b/internal/config.go index 94827ea..35de892 100644 --- a/internal/config.go +++ b/internal/config.go @@ -10,6 +10,7 @@ import ( "strings" "sync" + pb "git.nevets.tech/Keys/CertManager/proto/v1" "github.com/google/uuid" "github.com/spf13/viper" ) @@ -186,6 +187,18 @@ func SaveDomainConfigs() error { return nil } +// --------------------------------------------------------------------------- +// Domain Specific Lookups +// --------------------------------------------------------------------------- + +func PostPullHooks(domain string) ([]*pb.Hook, error) { + var hooks []*pb.Hook + if err := viper.UnmarshalKey("Hooks.PostPull", hooks); err != nil { + return nil, err + } + return hooks, nil +} + // --------------------------------------------------------------------------- // Effective lookups (domain → global fallback) // --------------------------------------------------------------------------- @@ -297,7 +310,7 @@ func CreateDomainConfig(domain string) error { "{domain}", domain, "{key}", key, ).Replace(defaultServerDomainConfig) - case "Client": + case "client": content = strings.NewReplacer( "{domain}", domain, "{key}", key, @@ -393,6 +406,12 @@ crypto_key = '{key}' domain_name = '{domain}' enabled = true +[Hooks.PostPull] +command = [] +cwd = "/dev/null" +timeout_seconds = 30 +env = { "FOO" = "bar" } + [Repo] repo_suffix = '-certificates' ` diff --git a/internal/grpc.go b/internal/grpc.go new file mode 100644 index 0000000..5bf0569 --- /dev/null +++ b/internal/grpc.go @@ -0,0 +1 @@ +package internal diff --git a/internal/util.go b/internal/util.go index df72fa9..b09e06b 100644 --- a/internal/util.go +++ b/internal/util.go @@ -5,6 +5,7 @@ import ( "fmt" "io/fs" "os" + "os/user" "path/filepath" "strconv" "strings" @@ -224,12 +225,12 @@ func LinkFile(source, target, domain, extension string) error { } if linkInfo.IsDir() { target = filepath.Join(target, domain+extension) + err = os.Symlink(source, target) + if err != nil { + return err + } } - err = os.Symlink(source, target) - if err != nil { - return err - } return nil } @@ -295,3 +296,64 @@ func ChownRecursive(path string, uid, gid int) error { return os.Chown(name, uid, gid) }) } + +func LookupGID(group string) (int, error) { + g, err := user.LookupGroup(group) + if err != nil { + return 0, err + } + return strconv.Atoi(g.Gid) +} + +// MakeCredential resolves username/groupname to uid/gid for syscall.Credential. +// Note: actually *using* different credentials typically requires the server +// process to have appropriate privileges (often root). +func MakeCredential(username, groupname string) (*syscall.Credential, error) { + var uid, gid uint32 + var haveUID, haveGID bool + + if username != "" { + u, err := user.Lookup(username) + if err != nil { + return nil, fmt.Errorf("unknown user") + } + parsed, err := strconv.ParseUint(u.Uid, 10, 32) + if err != nil { + return nil, fmt.Errorf("bad uid") + } + uid = uint32(parsed) + haveUID = true + + // If group not explicitly provided, default to user's primary group. + if groupname == "" && u.Gid != "" { + parsedG, err := strconv.ParseUint(u.Gid, 10, 32) + if err == nil { + gid = uint32(parsedG) + haveGID = true + } + } + } + + if groupname != "" { + g, err := user.LookupGroup(groupname) + if err != nil { + return nil, fmt.Errorf("unknown group") + } + parsed, err := strconv.ParseUint(g.Gid, 10, 32) + if err != nil { + return nil, fmt.Errorf("bad gid") + } + gid = uint32(parsed) + haveGID = true + } + + // If only group was provided, keep current uid. + if !haveUID { + uid = uint32(os.Getuid()) + } + if !haveGID { + gid = uint32(os.Getgid()) + } + + return &syscall.Credential{Uid: uid, Gid: gid}, nil +} diff --git a/main.go b/main.go index 5b1b175..a0d499e 100644 --- a/main.go +++ b/main.go @@ -99,6 +99,14 @@ func main() { rootCmd.AddCommand(certCmd) + rootCmd.AddCommand(&cobra.Command{ + Use: "executor", + Short: "Privileged daemon", + RunE: func(cmd *cobra.Command, args []string) error { + return commands.StartExecutorCmd() + }, + }) + daemonCmd := &cobra.Command{ Use: "daemon", Short: "Daemon management", diff --git a/proto/hook.proto b/proto/hook.proto new file mode 100644 index 0000000..567d7ae --- /dev/null +++ b/proto/hook.proto @@ -0,0 +1,27 @@ +syntax = "proto3"; + +package hooks.v1; + +option go_package = "/v1"; + +service HookService { + rpc ExecuteHook(ExecuteHookRequest) returns (ExecuteHookResponse); +} + +message Hook { + string name = 1; + repeated string command = 2; + string user = 3; + string group = 4; + string cwd = 5; + int32 timeout_seconds = 6; + map env = 7; +} + +message ExecuteHookRequest { + Hook hook = 1; +} + +message ExecuteHookResponse { + string error = 1; +} \ No newline at end of file diff --git a/proto/symlink.proto b/proto/symlink.proto new file mode 100644 index 0000000..4b61bb4 --- /dev/null +++ b/proto/symlink.proto @@ -0,0 +1,5 @@ +syntax = "proto3"; + +package hooks.v1; + +option go_package = "/v1"; \ No newline at end of file diff --git a/proto/v1/hook.pb.go b/proto/v1/hook.pb.go new file mode 100644 index 0000000..ba4f1ad --- /dev/null +++ b/proto/v1/hook.pb.go @@ -0,0 +1,280 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.30.2 +// source: proto/hook.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +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 Hook struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Command []string `protobuf:"bytes,2,rep,name=command,proto3" json:"command,omitempty"` + User string `protobuf:"bytes,3,opt,name=user,proto3" json:"user,omitempty"` + Group string `protobuf:"bytes,4,opt,name=group,proto3" json:"group,omitempty"` + Cwd string `protobuf:"bytes,5,opt,name=cwd,proto3" json:"cwd,omitempty"` + TimeoutSeconds int32 `protobuf:"varint,6,opt,name=timeout_seconds,json=timeoutSeconds,proto3" json:"timeout_seconds,omitempty"` + Env map[string]string `protobuf:"bytes,7,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Hook) Reset() { + *x = Hook{} + mi := &file_proto_hook_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Hook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Hook) ProtoMessage() {} + +func (x *Hook) ProtoReflect() protoreflect.Message { + mi := &file_proto_hook_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Hook.ProtoReflect.Descriptor instead. +func (*Hook) Descriptor() ([]byte, []int) { + return file_proto_hook_proto_rawDescGZIP(), []int{0} +} + +func (x *Hook) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Hook) GetCommand() []string { + if x != nil { + return x.Command + } + return nil +} + +func (x *Hook) GetUser() string { + if x != nil { + return x.User + } + return "" +} + +func (x *Hook) GetGroup() string { + if x != nil { + return x.Group + } + return "" +} + +func (x *Hook) GetCwd() string { + if x != nil { + return x.Cwd + } + return "" +} + +func (x *Hook) GetTimeoutSeconds() int32 { + if x != nil { + return x.TimeoutSeconds + } + return 0 +} + +func (x *Hook) GetEnv() map[string]string { + if x != nil { + return x.Env + } + return nil +} + +type ExecuteHookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Hook *Hook `protobuf:"bytes,1,opt,name=hook,proto3" json:"hook,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteHookRequest) Reset() { + *x = ExecuteHookRequest{} + mi := &file_proto_hook_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteHookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteHookRequest) ProtoMessage() {} + +func (x *ExecuteHookRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_hook_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteHookRequest.ProtoReflect.Descriptor instead. +func (*ExecuteHookRequest) Descriptor() ([]byte, []int) { + return file_proto_hook_proto_rawDescGZIP(), []int{1} +} + +func (x *ExecuteHookRequest) GetHook() *Hook { + if x != nil { + return x.Hook + } + return nil +} + +type ExecuteHookResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExecuteHookResponse) Reset() { + *x = ExecuteHookResponse{} + mi := &file_proto_hook_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExecuteHookResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExecuteHookResponse) ProtoMessage() {} + +func (x *ExecuteHookResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_hook_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExecuteHookResponse.ProtoReflect.Descriptor instead. +func (*ExecuteHookResponse) Descriptor() ([]byte, []int) { + return file_proto_hook_proto_rawDescGZIP(), []int{2} +} + +func (x *ExecuteHookResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +var File_proto_hook_proto protoreflect.FileDescriptor + +const file_proto_hook_proto_rawDesc = "" + + "\n" + + "\x10proto/hook.proto\x12\bhooks.v1\"\xfc\x01\n" + + "\x04Hook\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + + "\acommand\x18\x02 \x03(\tR\acommand\x12\x12\n" + + "\x04user\x18\x03 \x01(\tR\x04user\x12\x14\n" + + "\x05group\x18\x04 \x01(\tR\x05group\x12\x10\n" + + "\x03cwd\x18\x05 \x01(\tR\x03cwd\x12'\n" + + "\x0ftimeout_seconds\x18\x06 \x01(\x05R\x0etimeoutSeconds\x12)\n" + + "\x03env\x18\a \x03(\v2\x17.hooks.v1.Hook.EnvEntryR\x03env\x1a6\n" + + "\bEnvEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"8\n" + + "\x12ExecuteHookRequest\x12\"\n" + + "\x04hook\x18\x01 \x01(\v2\x0e.hooks.v1.HookR\x04hook\"+\n" + + "\x13ExecuteHookResponse\x12\x14\n" + + "\x05error\x18\x01 \x01(\tR\x05error2Y\n" + + "\vHookService\x12J\n" + + "\vExecuteHook\x12\x1c.hooks.v1.ExecuteHookRequest\x1a\x1d.hooks.v1.ExecuteHookResponseB\x05Z\x03/v1b\x06proto3" + +var ( + file_proto_hook_proto_rawDescOnce sync.Once + file_proto_hook_proto_rawDescData []byte +) + +func file_proto_hook_proto_rawDescGZIP() []byte { + file_proto_hook_proto_rawDescOnce.Do(func() { + file_proto_hook_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_proto_hook_proto_rawDesc), len(file_proto_hook_proto_rawDesc))) + }) + return file_proto_hook_proto_rawDescData +} + +var file_proto_hook_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_proto_hook_proto_goTypes = []any{ + (*Hook)(nil), // 0: hooks.v1.Hook + (*ExecuteHookRequest)(nil), // 1: hooks.v1.ExecuteHookRequest + (*ExecuteHookResponse)(nil), // 2: hooks.v1.ExecuteHookResponse + nil, // 3: hooks.v1.Hook.EnvEntry +} +var file_proto_hook_proto_depIdxs = []int32{ + 3, // 0: hooks.v1.Hook.env:type_name -> hooks.v1.Hook.EnvEntry + 0, // 1: hooks.v1.ExecuteHookRequest.hook:type_name -> hooks.v1.Hook + 1, // 2: hooks.v1.HookService.ExecuteHook:input_type -> hooks.v1.ExecuteHookRequest + 2, // 3: hooks.v1.HookService.ExecuteHook:output_type -> hooks.v1.ExecuteHookResponse + 3, // [3:4] is the sub-list for method output_type + 2, // [2:3] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_proto_hook_proto_init() } +func file_proto_hook_proto_init() { + if File_proto_hook_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_hook_proto_rawDesc), len(file_proto_hook_proto_rawDesc)), + NumEnums: 0, + NumMessages: 4, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_proto_hook_proto_goTypes, + DependencyIndexes: file_proto_hook_proto_depIdxs, + MessageInfos: file_proto_hook_proto_msgTypes, + }.Build() + File_proto_hook_proto = out.File + file_proto_hook_proto_goTypes = nil + file_proto_hook_proto_depIdxs = nil +} diff --git a/proto/v1/hook_grpc.pb.go b/proto/v1/hook_grpc.pb.go new file mode 100644 index 0000000..5ad9f05 --- /dev/null +++ b/proto/v1/hook_grpc.pb.go @@ -0,0 +1,121 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc v6.30.2 +// source: proto/hook.proto + +package v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + HookService_ExecuteHook_FullMethodName = "/hooks.v1.HookService/ExecuteHook" +) + +// HookServiceClient is the client API for HookService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type HookServiceClient interface { + ExecuteHook(ctx context.Context, in *ExecuteHookRequest, opts ...grpc.CallOption) (*ExecuteHookResponse, error) +} + +type hookServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewHookServiceClient(cc grpc.ClientConnInterface) HookServiceClient { + return &hookServiceClient{cc} +} + +func (c *hookServiceClient) ExecuteHook(ctx context.Context, in *ExecuteHookRequest, opts ...grpc.CallOption) (*ExecuteHookResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExecuteHookResponse) + err := c.cc.Invoke(ctx, HookService_ExecuteHook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// HookServiceServer is the server API for HookService service. +// All implementations must embed UnimplementedHookServiceServer +// for forward compatibility. +type HookServiceServer interface { + ExecuteHook(context.Context, *ExecuteHookRequest) (*ExecuteHookResponse, error) + mustEmbedUnimplementedHookServiceServer() +} + +// UnimplementedHookServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedHookServiceServer struct{} + +func (UnimplementedHookServiceServer) ExecuteHook(context.Context, *ExecuteHookRequest) (*ExecuteHookResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ExecuteHook not implemented") +} +func (UnimplementedHookServiceServer) mustEmbedUnimplementedHookServiceServer() {} +func (UnimplementedHookServiceServer) testEmbeddedByValue() {} + +// UnsafeHookServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to HookServiceServer will +// result in compilation errors. +type UnsafeHookServiceServer interface { + mustEmbedUnimplementedHookServiceServer() +} + +func RegisterHookServiceServer(s grpc.ServiceRegistrar, srv HookServiceServer) { + // If the following call panics, it indicates UnimplementedHookServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&HookService_ServiceDesc, srv) +} + +func _HookService_ExecuteHook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExecuteHookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(HookServiceServer).ExecuteHook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: HookService_ExecuteHook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(HookServiceServer).ExecuteHook(ctx, req.(*ExecuteHookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// HookService_ServiceDesc is the grpc.ServiceDesc for HookService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var HookService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hooks.v1.HookService", + HandlerType: (*HookServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ExecuteHook", + Handler: _HookService_ExecuteHook_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "proto/hook.proto", +} diff --git a/proto/v1/symlink.pb.go b/proto/v1/symlink.pb.go new file mode 100644 index 0000000..5e51f6f --- /dev/null +++ b/proto/v1/symlink.pb.go @@ -0,0 +1,59 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc v6.30.2 +// source: proto/symlink.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + unsafe "unsafe" +) + +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) +) + +var File_proto_symlink_proto protoreflect.FileDescriptor + +const file_proto_symlink_proto_rawDesc = "" + + "\n" + + "\x13proto/symlink.proto\x12\bhooks.v1B\x05Z\x03/v1b\x06proto3" + +var file_proto_symlink_proto_goTypes = []any{} +var file_proto_symlink_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_proto_symlink_proto_init() } +func file_proto_symlink_proto_init() { + if File_proto_symlink_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_symlink_proto_rawDesc), len(file_proto_symlink_proto_rawDesc)), + NumEnums: 0, + NumMessages: 0, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_proto_symlink_proto_goTypes, + DependencyIndexes: file_proto_symlink_proto_depIdxs, + }.Build() + File_proto_symlink_proto = out.File + file_proto_symlink_proto_goTypes = nil + file_proto_symlink_proto_depIdxs = nil +}