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
69 changes: 54 additions & 15 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 @@ -23,10 +24,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 @@ -48,26 +49,38 @@ type JWTConfig struct {

// 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
}

// ParseConfigFlagOverrides handles command line arguments that override config file settings
func (c *Config) ParseConfigFlagOverrides(daemonModeFlag bool, daemonModeFlagName string) {
if isFlagPassed(daemonModeFlagName) {
// 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
}
}

func ValidateConfig(c *Config, exitWhenReady bool, log logrus.FieldLogger) error {
func (c *Config) ValidateConfig(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 +153,15 @@ func ValidateConfig(c *Config, exitWhenReady bool, log logrus.FieldLogger) error
}
}

c.ExitWhenReady = c.ExitWhenReady || exitWhenReady

x509EmptyCount := countEmpty(c.SVIDFileName, c.SVIDBundleFileName, c.SVIDKeyFileName)
jwtBundleEmptyCount := countEmpty(c.SVIDBundleFileName)
if x509EmptyCount == 3 && len(c.JWTSVIDs) == 0 && jwtBundleEmptyCount == 1 {
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")
x509Enabled, err := validateX509Config(c)
if err != nil {
return err
}

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")
jwtBundleEnabled, jwtSVIDsEnabled := validateJWTConfig(c)

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")
}

return nil
Expand All @@ -162,7 +174,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 +193,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 +219,18 @@ func countEmpty(configs ...string) int {
cnt++
}
}

return cnt
}

// isFlagPassed tests to see if a command line argument was set at all or left empty
func isFlagPassed(name string) bool {
var found bool
flag.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})

return found
}
23 changes: 17 additions & 6 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 All @@ -10,6 +11,10 @@ import (
"github.com/stretchr/testify/require"
)

const (
daemonModeFlagName = "daemon-mode"
)

func TestParseConfig(t *testing.T) {
c, err := ParseConfig("testdata/helper.conf")

Expand Down Expand Up @@ -294,7 +299,7 @@ func TestValidateConfig(t *testing.T) {
} {
t.Run(tt.name, func(t *testing.T) {
log, hook := test.NewNullLogger()
err := ValidateConfig(tt.config, false, log)
err := tt.config.ValidateConfig(log)

require.ElementsMatch(t, tt.expectLogs, getShortEntries(hook.AllEntries()))

Expand Down Expand Up @@ -345,7 +350,7 @@ func TestDefaultAgentAddress(t *testing.T) {
SVIDBundleFileName: "bundle.pem",
}
log, _ := test.NewNullLogger()
err := ValidateConfig(config, false, log)
err := config.ValidateConfig(log)
require.NoError(t, err)
assert.Equal(t, config.AgentAddress, tt.expectedAgentAddress)
})
Expand Down Expand Up @@ -388,16 +393,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)

daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit")
flag.Parse()

err := flag.Set(daemonModeFlagName, "false")
require.NoError(t, err)
assert.Equal(t, config.ExitWhenReady, true)

config.ParseConfigFlagOverrides(*daemonModeFlag, daemonModeFlagName)
require.NotNil(t, config.DaemonMode)
assert.Equal(t, false, *config.DaemonMode)
}

type shortEntry struct {
Expand Down
34 changes: 21 additions & 13 deletions cmd/spiffe-helper/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,40 +12,48 @@ import (
"github.com/spiffe/spiffe-helper/pkg/sidecar"
)

func main() {
// 0. Load configuration
// 1. Create Sidecar
// 2. Run Sidecar's Daemon
const (
daemonModeFlagName = "daemon-mode"
)

func main() {
configFile := flag.String("config", "helper.conf", "<configFile> Configuration file path")
exitWhenReady := flag.Bool("exitWhenReady", false, "Exit once the requested objects are retrieved")
daemonModeFlag := flag.Bool(daemonModeFlagName, true, "Toggle running as a daemon to rotate X.509/JWT or just fetch and exit")
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(*configFile, *daemonModeFlag, 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(configFile string, daemonModeFlag bool, log logrus.FieldLogger) error {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

hclConfig, err := config.ParseConfig(configPath)
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)
}
if err := config.ValidateConfig(hclConfig, exitWhenReady, log); err != nil {
hclConfig.ParseConfigFlagOverrides(daemonModeFlag, daemonModeFlagName)

if err := hclConfig.ValidateConfig(log); err != nil {
return fmt.Errorf("invalid configuration: %w", err)
}

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.24.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