Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add daemon mode flag #161

Merged
merged 14 commits into from
Aug 27, 2024
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ The configuration file is an [HCL](https://github.com/hashicorp/hcl) formatted f
| `cmd` | The path to the process to launch. | `"ghostunnel"` |
| `cmd_args` | The arguments of the process to launch. | `"server --listen localhost:8002 --target localhost:8001--keystore certs/svid_key.pem --cacert certs/svid_bundle.pem --allow-uri-san spiffe://example.org/Database"` |
| `cert_dir` | Directory name to store the fetched certificates. This directory must be created previously. | `"certs"` |
| `exit_when_ready` | Fetch x509 certificate and then exit(0) | `true` |
| `daemon_mode` | Toggle running as a daemon, keeping X.509 and JWT up to date; or just fetch X.509 and JWT and exit 0 | `true` |
| `add_intermediates_to_bundle` | Add intermediate certificates into Bundle file instead of SVID file. | `true` |
| `renew_signal` | The signal that the process to be launched expects to reload the certificates. It is not supported on Windows. | `"SIGUSR1"` |
| `svid_file_name` | File name to be used to store the X.509 SVID public certificate in PEM format. | `"svid.pem"` |
Expand Down
70 changes: 57 additions & 13 deletions cmd/spiffe-helper/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package config

import (
"errors"
"flag"
"os"

"github.com/hashicorp/hcl"
Expand All @@ -11,6 +12,7 @@ import (

const (
defaultAgentAddress = "/tmp/spire-agent/public/api.sock"
daemonModeFlagName = "daemon-mode"
)

type Config struct {
Expand All @@ -23,10 +25,10 @@ type Config struct {
CmdArgsDeprecated string `hcl:"cmdArgs"`
CertDir string `hcl:"cert_dir"`
CertDirDeprecated string `hcl:"certDir"`
ExitWhenReady bool `hcl:"exit_when_ready"`
IncludeFederatedDomains bool `hcl:"include_federated_domains"`
RenewSignal string `hcl:"renew_signal"`
RenewSignalDeprecated string `hcl:"renewSignal"`
DaemonMode *bool `hcl:"daemon_mode"`

// x509 configuration
SVIDFileName string `hcl:"svid_file_name"`
Expand All @@ -46,28 +48,36 @@ type JWTConfig struct {
JWTSVIDFilename string `hcl:"jwt_svid_file_name"`
}

func ParseFlags() (string, bool) {
configFile := flag.String("config", "helper.conf", "<configFile> Configuration file path")
daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Exit once the requested objects are retrieved")
faisal-memon marked this conversation as resolved.
Show resolved Hide resolved
flag.Parse()
faisal-memon marked this conversation as resolved.
Show resolved Hide resolved

return *configFile, *daemonModeFlag
}

// ParseConfig parses the given HCL file into a Config struct
func ParseConfig(file string) (*Config, error) {
sidecarConfig := new(Config)

// Read HCL file
dat, err := os.ReadFile(file)
if err != nil {
return nil, err
}

// Parse HCL
if err := hcl.Decode(sidecarConfig, string(dat)); err != nil {
config := new(Config)
if err := hcl.Decode(config, string(dat)); err != nil {
return nil, err
}

return sidecarConfig, nil
return config, nil
}

func ValidateConfig(c *Config, exitWhenReady bool, log logrus.FieldLogger) error {
func ValidateConfig(c *Config, daemonModeFlag bool, log logrus.FieldLogger) error {
if err := validateOSConfig(c); err != nil {
return err
}

if c.AgentAddressDeprecated != "" {
if c.AgentAddress != "" {
return errors.New("use of agent_address and agentAddress found, use only agent_address")
Expand Down Expand Up @@ -140,16 +150,24 @@ func ValidateConfig(c *Config, exitWhenReady bool, log logrus.FieldLogger) error
}
}

c.ExitWhenReady = c.ExitWhenReady || exitWhenReady
x509Enabled, err := validateX509Config(c)
if err != nil {
return err
}

jwtBundleEnabled, jwtSVIDsEnabled := validateJWTConfig(c)

x509EmptyCount := countEmpty(c.SVIDFileName, c.SVIDBundleFileName, c.SVIDKeyFileName)
jwtBundleEmptyCount := countEmpty(c.SVIDBundleFileName)
if x509EmptyCount == 3 && len(c.JWTSVIDs) == 0 && jwtBundleEmptyCount == 1 {
if !x509Enabled && !jwtBundleEnabled && !jwtSVIDsEnabled {
return errors.New("at least one of the sets ('svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name'), 'jwt_svids', or 'jwt_bundle_file_name' must be fully specified")
}

if x509EmptyCount != 0 && x509EmptyCount != 3 {
return errors.New("all or none of 'svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name' must be specified")
if isFlagPassed(daemonModeFlagName) {
faisal-memon marked this conversation as resolved.
Show resolved Hide resolved
// If daemon mode is set by CLI this takes precedence
c.DaemonMode = &daemonModeFlag
} else if c.DaemonMode == nil {
// If daemon mode is not set, then default to true
daemonMode := true
c.DaemonMode = &daemonMode
}

return nil
Expand All @@ -162,7 +180,6 @@ func NewSidecarConfig(config *Config, log logrus.FieldLogger) *sidecar.Config {
Cmd: config.Cmd,
CmdArgs: config.CmdArgs,
CertDir: config.CertDir,
ExitWhenReady: config.ExitWhenReady,
IncludeFederatedDomains: config.IncludeFederatedDomains,
JWTBundleFilename: config.JWTBundleFilename,
Log: log,
Expand All @@ -182,6 +199,21 @@ func NewSidecarConfig(config *Config, log logrus.FieldLogger) *sidecar.Config {
return sidecarConfig
}

func validateX509Config(c *Config) (bool, error) {
x509EmptyCount := countEmpty(c.SVIDFileName, c.SVIDBundleFileName, c.SVIDKeyFileName)
if x509EmptyCount != 0 && x509EmptyCount != 3 {
return false, errors.New("all or none of 'svid_file_name', 'svid_key_file_name', 'svid_bundle_file_name' must be specified")
}

return x509EmptyCount == 0, nil
}

func validateJWTConfig(c *Config) (bool, bool) {
jwtBundleEmptyCount := countEmpty(c.SVIDBundleFileName)

return jwtBundleEmptyCount == 0, len(c.JWTSVIDs) > 0
}

func getWarning(s1 string, s2 string) string {
return s1 + " will be deprecated, should be used as " + s2
}
Expand All @@ -193,5 +225,17 @@ func countEmpty(configs ...string) int {
cnt++
}
}

return cnt
}

func isFlagPassed(name string) bool {
var found bool
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})

return found
}
13 changes: 10 additions & 3 deletions cmd/spiffe-helper/config/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package config

import (
"flag"
"os"
"testing"

Expand Down Expand Up @@ -388,16 +389,22 @@ func TestNewSidecarConfig(t *testing.T) {
assert.Equal(t, "", sidecarConfig.RenewSignal)
}

func TestExitOnWaitFlag(t *testing.T) {
func TestDaemonModeFlag(t *testing.T) {
config := &Config{
SVIDFileName: "cert.pem",
SVIDKeyFileName: "key.pem",
SVIDBundleFileName: "bundle.pem",
}
log, _ := test.NewNullLogger()
err := ValidateConfig(config, true, log)

_, _ = ParseFlags()
err := flag.Set(daemonModeFlagName, "false")
require.NoError(t, err)

err = ValidateConfig(config, false, log)
require.NoError(t, err)
assert.Equal(t, config.ExitWhenReady, true)
require.NotNil(t, config.DaemonMode)
assert.Equal(t, false, *config.DaemonMode)
}

type shortEntry struct {
Expand Down
35 changes: 18 additions & 17 deletions cmd/spiffe-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"context"
"flag"
"fmt"
"os"
"os/signal"
Expand All @@ -13,39 +12,41 @@ import (
)

func main() {
// 0. Load configuration
// 1. Create Sidecar
// 2. Run Sidecar's Daemon

configFile := flag.String("config", "helper.conf", "<configFile> Configuration file path")
exitWhenReady := flag.Bool("exitWhenReady", false, "Exit once the requested objects are retrieved")
flag.Parse()

log := logrus.WithField("system", "spiffe-helper")
log.Infof("Using configuration file: %q\n", *configFile)

if err := startSidecar(*configFile, *exitWhenReady, log); err != nil {
log.WithError(err).Error("Exiting due this error")
if err := startSidecar(log); err != nil {
log.WithError(err).Errorf("Error starting spiffe-helper")
os.Exit(1)
}

log.Infof("Exiting")
os.Exit(0)
}

func startSidecar(configPath string, exitWhenReady bool, log logrus.FieldLogger) error {
func startSidecar(log logrus.FieldLogger) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

hclConfig, err := config.ParseConfig(configPath)
configFile, daemonModeFlag := config.ParseFlags()
log.Infof("Using configuration file: %q", configFile)

hclConfig, err := config.ParseConfig(configFile)
if err != nil {
return fmt.Errorf("failed to parse %q: %w", configPath, err)
return fmt.Errorf("Failed to parse %q: %w", configFile, err)
faisal-memon marked this conversation as resolved.
Show resolved Hide resolved
}
if err := config.ValidateConfig(hclConfig, exitWhenReady, log); err != nil {
return fmt.Errorf("invalid configuration: %w", err)

if err := config.ValidateConfig(hclConfig, daemonModeFlag, log); err != nil {
return fmt.Errorf("Invalid configuration: %w", err)
faisal-memon marked this conversation as resolved.
Show resolved Hide resolved
}

sidecarConfig := config.NewSidecarConfig(hclConfig, log)
spiffeSidecar := sidecar.New(sidecarConfig)

if !*hclConfig.DaemonMode {
log.Info("Daemon mode disabled")
return spiffeSidecar.Run(ctx)
}

log.Info("Launching daemon")
return spiffeSidecar.RunDaemon(ctx)
}
56 changes: 56 additions & 0 deletions examples/k8s/helper-no-daemon.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: spiffe-helper
---
apiVersion: v1
kind: ConfigMap
metadata:
name: spiffe-helper
data:
helper.conf: |
cmd = ""
cmd_args = ""
cert_dir = ""
renew_signal = ""
svid_file_name = "tls.crt"
svid_key_file_name = "tls.key"
svid_bundle_file_name = "ca.pem"
jwt_bundle_file_name = "cert.jwt"
jwt_svids = [{jwt_audience="test", jwt_svid_file_name="jwt_svid.token"}]
daemon_mode = false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spiffe-helper
labels:
app: spiffe-helper
spec:
selector:
matchLabels:
app: spiffe-helper
template:
metadata:
labels:
app: spiffe-helper
spec:
serviceAccountName: spiffe-helper
containers:
- name: spiffe-helper
image: ghcr.io/spiffe/spiffe-helper:devel
args: ["-config", "config/helper.conf"]
volumeMounts:
- name: spire-agent-socket
mountPath: /tmp/spire-agent/public/
readOnly: true
- name: helper-config
mountPath: /config
volumes:
- name: spire-agent-socket
hostPath:
path: /run/spire/agent-sockets
type: Directory
- name: helper-config
configMap:
name: spiffe-helper
14 changes: 14 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,25 @@ require (
github.com/stretchr/testify v1.9.0
golang.org/x/sys v0.21.0
google.golang.org/grpc v1.65.0
k8s.io/apimachinery v0.30.1
k8s.io/client-go v0.30.1
)

require (
github.com/go-jose/go-jose/v4 v4.0.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
)

require (
Expand Down
Loading
Loading