Skip to content

Commit

Permalink
Add infrastructure for loading certs on the server (#86)
Browse files Browse the repository at this point in the history
* Move the expect package

* Refactor Makefile for better file handling

The Makefile has been updated to improve the way it handles '.go' files. The changes include:
- Adjusted the GO_SRC variable to exclude './' from the start of file paths.
- Added new targets under .make/tidy/ for each Go module (gen, provider, sdk, tests) to ensure their dependencies are tidied up correctly.

* Go crap

* Create certs and put them in the container

* Added TLS support to gRPC server

- Introduced a new function to load certificates for TLS configuration
- Added a new option in the provisioner struct for gRPC server options
- Modified the New function to include these server options when creating a new gRPC server
- Created an opt function WithTLS that appends TLS credentials to the gRPC server options
- Renamed RegisterCommandServiceServer and State functions for better readability

* Added NoOp and If functions to opts

Two new utility functions have been added to the opts package. The NoOp function is a no-operation function that returns an error-free function for any type. The If function takes a boolean predicate and an option, returning the option if the predicate is true or a no-op otherwise.

* Refactor provisioner with error handling and options

Significant changes include:
- Added error handling to the 'New' function in the provisioner package.
- Introduced new functions for setting gRPC options and TLS configuration.
- Created a function to handle optional certificates, which only applies if all certificate files are provided.
- Refactored existing functions to use these new option-setting functions.

* Enhanced logging and added optional certificate support

The provisioner's main.go file has been updated to enhance the logging system. The log level is now set based on the verbose flag, and error messages have been improved for better clarity. Additionally, support for optional certificates (CA, server certificate, and private key) has been introduced. These can be provided via new command line flags: 'ca-file', 'cert-file', and 'key-file'.
  • Loading branch information
UnstoppableMango authored Aug 2, 2024
1 parent 23f3f54 commit 1302582
Show file tree
Hide file tree
Showing 15 changed files with 304 additions and 67 deletions.
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ MANS := tee
MAN_SRC := $(MANS:%=$(PKG_DIR)/provider/cmd/%.man)

GO_MODULES := gen provider sdk tests
GO_SRC := $(shell find . -type f -name '*.go')
GO_SRC := $(subst ./,,$(shell find . -type f -name '*.go'))
PROVIDER_SRC := $(filter $(PROVIDER_PATH)/%,$(GO_SRC))
PKG_SRC := $(filter $(PKG_DIR)/%,$(GO_SRC))
PROTO_SRC := $(shell find $(PROTO_DIR) -type f -name '*.proto')
Expand Down Expand Up @@ -184,6 +184,10 @@ provider/pkg/%.man: provider/pkg/%.go
buf.lock: $(BUF_CONFIG)
buf dep update

.make/tidy/gen: $(filter gen/%,$(GO_SRC))
.make/tidy/provider: $(filter provider/%,$(GO_SRC))
.make/tidy/sdk: $(filter sdk/%,$(GO_SRC))
.make/tidy/tests: $(filter tests/%,$(GO_SRC))
$(GO_MODULES:%=.make/tidy/%): .make/tidy/%: $(addprefix %/,go.mod go.sum)
go -C $* mod tidy
@touch $@
Expand Down
66 changes: 40 additions & 26 deletions provider/cmd/provisioner/main.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"fmt"
"log/slog"
"net"
"os"
Expand All @@ -10,53 +11,66 @@ import (
)

var (
address string
network string
verbose bool
log *slog.Logger
address string
network string
caFile string
certFile string
keyFile string
verbose bool
)

var rootCmd = &cobra.Command{
Use: "provisioner",
Short: "The pulumi-baremetal provisioner",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
log = logger(verbose)
},
RunE: func(cmd *cobra.Command, args []string) error {
var level slog.Level
if verbose {
level = slog.LevelDebug
}

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
})

log := slog.New(handler)
lis, err := net.Listen(network, address)
if err != nil {
return err
return fmt.Errorf("failed to create listener: %w", err)
}

log.Debug("creating provisioner")
provisioner := p.New(lis, p.WithLogger(log))
provisioner := p.New(lis,
p.WithLogger(log),
p.WithOptionalCertificates(caFile, certFile, keyFile),
)

log.Info("serving",
"network", network,
"address", address,
"verbose", verbose,
"caFile", caFile,
"certFile", certFile,
"keyFile", keyFile,
)

log.Info("serving", "network", network, "address", address, "verbose", verbose)
return provisioner.Serve()
},
}

func main() {
rootCmd.Flags().StringVar(&address, "address", "", "Must be a valid `net.Listen()` address.")
rootCmd.Flags().StringVar(&address, "address", "", "Must be a valid `net.Listen()` address")
rootCmd.Flags().StringVar(&network, "network", "tcp", "Must be a valid `net.Listen()` network. i.e. \"tcp\", \"tcp4\", \"tcp6\", \"unix\" or \"unixpacket\"")
rootCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Log verbosity")

if err := rootCmd.Execute(); err != nil {
log.Error("failed to execute", "err", err)
}

log.Debug("exiting gracefully")
}
rootCmd.Flags().StringVar(&caFile, "ca-file", "", "The path to the certificate authority file")
rootCmd.Flags().StringVar(&certFile, "cert-file", "", "The path to the server certificate file")
rootCmd.Flags().StringVar(&keyFile, "key-file", "", "The path to the server private key file")
rootCmd.MarkFlagsRequiredTogether("ca-file", "cert-file", "key-file")

func logger(verbose bool) *slog.Logger {
var level slog.Level
if verbose {
level = slog.LevelDebug
if err := rootCmd.Execute(); err != nil {
fmt.Printf("failed to execute: %s\n", err)
os.Exit(1)
}

handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: level,
})

return slog.New(handler)
fmt.Println("exiting gracefully")
}
14 changes: 14 additions & 0 deletions provider/pkg/internal/opts/opts.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,17 @@ func Apply[V O[T], T any](x *T, opts ...V) error {

return nil
}

func NoOp[V O[T], T any]() V {
return func(*T) error {
return nil
}
}

func If[V O[T], T any](predicate bool, o V) V {
if predicate {
return o
} else {
return NoOp[V]()
}
}
57 changes: 50 additions & 7 deletions provider/pkg/provisioner/provisioner.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package provisioner

import (
"crypto/tls"
"fmt"
"log/slog"
"net"

Expand All @@ -9,10 +11,12 @@ import (
"github.com/unmango/pulumi-baremetal/provider/pkg/internal/opts"
"github.com/unmango/pulumi-baremetal/provider/pkg/provisioner/cmd"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)

type Options struct {
logger *slog.Logger
grpc []grpc.ServerOption
}

type opt func(*Options) error
Expand All @@ -29,19 +33,22 @@ type provisioner struct {

// Serve implements Provisioner.
func (p *provisioner) Serve() error {
p.RegisterCommandServiceServer(cmd.NewServer(p.State))
p.registerCommandService(cmd.NewServer(p.State))

return p.server.Serve(p.listener)
}

func New(lis net.Listener, o ...opt) Provisioner {
options := &Options{}
_ = opts.Apply(options, o...)
options := &Options{slog.Default(), []grpc.ServerOption{}}
err := opts.Apply(options, o...)
if err != nil {
panic(err) // TODO: Update the signature to return an error
}

return &provisioner{
State: options.State(),
State: options.state(),
listener: lis,
server: grpc.NewServer(),
server: grpc.NewServer(options.grpc...),
}
}

Expand All @@ -52,14 +59,50 @@ func WithLogger(logger *slog.Logger) opt {
}
}

func WithGrpcOption(opt grpc.ServerOption) opt {
return func(o *Options) error {
o.grpcOption(opt)
return nil
}
}

func WithTLS(config *tls.Config) opt {
return func(o *Options) error {
o.tlsConfig(config)
return nil
}
}

func WithOptionalCertificates(caFile, certFile, keyFile string) opt {
missingFile := caFile == "" || certFile == "" || keyFile == ""
return opts.If(!missingFile, func(o *Options) error {
certs, err := LoadCertificates(caFile, certFile, keyFile)
if err != nil {
return fmt.Errorf("failed to load certificates: %w", err)
}

o.tlsConfig(certs)
return nil
})
}

func Serve(lis net.Listener) error {
return New(lis).Serve()
}

func (o *Options) State() *internal.State {
func (o *Options) state() *internal.State {
return &internal.State{Log: o.logger}
}

func (p *provisioner) RegisterCommandServiceServer(srv pb.CommandServiceServer) {
func (o *Options) grpcOption(opt grpc.ServerOption) {
o.grpc = append(o.grpc, opt)
}

func (o *Options) tlsConfig(config *tls.Config) {
creds := credentials.NewTLS(config)
o.grpcOption(grpc.Creds(creds))
}

func (p *provisioner) registerCommandService(srv pb.CommandServiceServer) {
pb.RegisterCommandServiceServer(p.server, srv)
}
30 changes: 30 additions & 0 deletions provider/pkg/provisioner/tls.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package provisioner

import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
)

func LoadCertificates(caPath, certPath, keyPath string) (*tls.Config, error) {
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
if err != nil {
return nil, fmt.Errorf("failed loading keypair: %w", err)
}

ca := x509.NewCertPool()
caData, err := os.ReadFile(caPath)
if err != nil {
return nil, fmt.Errorf("failed reading ca file: %w", err)
}
if ok := ca.AppendCertsFromPEM(caData); ok {
return nil, fmt.Errorf("unable to append ca data from file %s", caPath)
}

return &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{cert},
ClientCAs: ca,
}, nil
}
2 changes: 1 addition & 1 deletion tests/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ var _ = Describe("Bootstrap", Ordered, func() {

BeforeAll(func(ctx context.Context) {
By("configuring the provider")
err := provisioner.ConfigureProvider(ctx, server)
err := util.ConfigureProvider(server).Configure()
Expect(err).NotTo(HaveOccurred())
})

Expand Down
File renamed without changes.
File renamed without changes.
3 changes: 2 additions & 1 deletion tests/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ replace github.com/unmango/pulumi-baremetal/provider => ../provider

require (
github.com/blang/semver v3.5.1+incompatible
github.com/mdelapenya/tlscert v0.1.0
github.com/onsi/ginkgo/v2 v2.19.1
github.com/onsi/gomega v1.34.1
github.com/pulumi/pulumi-go-provider v0.21.0
github.com/pulumi/pulumi/pkg/v3 v3.127.0
github.com/pulumi/pulumi/sdk/v3 v3.127.0
github.com/testcontainers/testcontainers-go v0.32.0
github.com/unmango/pulumi-baremetal/provider v0.0.0-00010101000000-000000000000
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
)

require (
Expand Down Expand Up @@ -204,7 +206,6 @@ require (
gocloud.dev v0.37.0 // indirect
gocloud.dev/secrets/hashivault v0.37.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.20.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions tests/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mdelapenya/tlscert v0.1.0 h1:YTpF579PYUX475eOL+6zyEO3ngLTOUWck78NBuJVXaM=
github.com/mdelapenya/tlscert v0.1.0/go.mod h1:wrbyM/DwbFCeCeqdPX/8c6hNOqQgbf0rUDErE1uD+64=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
Expand Down
22 changes: 17 additions & 5 deletions tests/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "github.com/unmango/pulumi-baremetal/tests/util/expect"
. "github.com/unmango/pulumi-baremetal/tests/expect"

p "github.com/pulumi/pulumi-go-provider"
"github.com/pulumi/pulumi-go-provider/integration"
Expand All @@ -27,12 +27,24 @@ var _ = Describe("Command Resources", func() {
By("creating an integration server")
server = util.NewServer()

By("configuring the provider")
err := provisioner.ConfigureProvider(ctx, server)
By("creating a workspace in the container")
_, err := provisioner.Exec(ctx, "mkdir", "-p", work)
Expect(err).NotTo(HaveOccurred())

By("creating a workspace in the container")
err = provisioner.Exec(ctx, "mkdir", "-p", work)
By("generating certificates")
certs, err := provisioner.CreateCertBundle(ctx, "lifecycle", work)
Expect(err).NotTo(HaveOccurred())

By("fetching provisioner connection details")
addr, port, err := provisioner.ConnectionDetails(ctx)
Expect(err).NotTo(HaveOccurred())

By("configuring the provider")
err = util.ConfigureProvider(server).
WithProvisioner(addr, port).
WithCerts(certs).
Configure()

Expect(err).NotTo(HaveOccurred())
})

Expand Down
Loading

0 comments on commit 1302582

Please sign in to comment.