diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 90ca2720..65b73b9f 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,12 +4,6 @@ "dockerComposeFile": "docker-compose.yaml", "service": "dev", "shutdownAction": "stopCompose", - "runServices": [ - "dev", - "haproxy", - "nats-auth", - "nats" - ], "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "customizations": { // Configure properties specific to VS Code. diff --git a/.devcontainer/docker-compose.yaml b/.devcontainer/docker-compose.yaml index a4aacaf8..cae249ce 100644 --- a/.devcontainer/docker-compose.yaml +++ b/.devcontainer/docker-compose.yaml @@ -5,8 +5,9 @@ services: context: . dockerfile: Dockerfile command: sleep infinity - depends_on: - - nats-init + environment: + LOADBALANCER_MANAGER_HAPROXY_EVENTS_SUBSCRIBER_NATS_CREDSFILE: /etc/nats-auth/manager.creds + LOADBALANCER_MANAGER_HAPROXY_EVENTS_SUBSCRIBER_PREFIX: com.infratographer volumes: - ..:/workspaces:cached - nats-auth:/etc/nats-auth @@ -53,6 +54,7 @@ services: "nats", "stream", "--server=nats", + "--creds=/etc/nats-auth/manager.creds", "add", "loadbalancer-manager-haproxy", "--subjects=com.infratographer.>", @@ -70,6 +72,8 @@ services: "--deny-delete", "--deny-purge" ] + volumes: + - nats-auth:/etc/nats-auth volumes: nats-auth: diff --git a/.devcontainer/setup-scripts/configure-nats b/.devcontainer/setup-scripts/configure-nats old mode 100644 new mode 100755 index c0dcf030..160ba740 --- a/.devcontainer/setup-scripts/configure-nats +++ b/.devcontainer/setup-scripts/configure-nats @@ -1,5 +1,4 @@ #!/usr/bin/env sh - set -e echo "Creating NATS Operator" @@ -7,12 +6,14 @@ nsc add operator -n TEST-OPERATOR --sys --generate-signing-key echo "Creating NATS Account" nsc add account -n TEST-ACCOUNT +nsc edit account TEST-ACCOUNT --js-mem-storage -1 --js-disk-storage -1 --js-streams -1 --js-consumer -1 +# --sk generate echo "Creating NATS User" nsc add user -a TEST-ACCOUNT -n MANAGER echo "Generate NATS server config" -mkdir /etc/nats-auth +mkdir -p /etc/nats-auth nsc generate config --mem-resolver --sys-account SYS > /etc/nats-auth/resolver.conf echo "Save NATS User creds" diff --git a/cmd/root.go b/cmd/root.go index 2f3118dd..d289e2c4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -8,7 +8,6 @@ import ( homedir "github.com/mitchellh/go-homedir" "github.com/spf13/cobra" - "github.com/spf13/pflag" "github.com/spf13/viper" "go.uber.org/zap" @@ -93,10 +92,3 @@ func setupAppConfig() { os.Exit(1) } } - -// viperBindFlag provides a wrapper around the viper bindings that handles error checks -func viperBindFlag(name string, flag *pflag.Flag) { - if err := viper.BindPFlag(name, flag); err != nil { - panic(err) - } -} diff --git a/cmd/run.go b/cmd/run.go index 758f13c9..0dd28f39 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -8,6 +8,7 @@ import ( "go.infratographer.com/x/events" "go.infratographer.com/x/gidx" + "go.infratographer.com/x/viperx" "go.uber.org/zap" "go.infratographer.com/loadbalancer-manager-haproxy/internal/config" @@ -15,6 +16,7 @@ import ( "go.infratographer.com/loadbalancer-manager-haproxy/internal/manager" "go.infratographer.com/loadbalancer-manager-haproxy/internal/pubsub" "go.infratographer.com/loadbalancer-manager-haproxy/pkg/lbapi" + "go.infratographer.com/loadbalancer-manager-haproxy/x/oauth2x" "github.com/spf13/cobra" "github.com/spf13/viper" @@ -33,27 +35,28 @@ func init() { rootCmd.AddCommand(runCmd) runCmd.PersistentFlags().StringSlice("events-topics", []string{}, "event topics to subscribe to") - viperBindFlag("events.topics", runCmd.PersistentFlags().Lookup("events-topics")) + viperx.MustBindFlag(viper.GetViper(), "events.topics", runCmd.PersistentFlags().Lookup("events-topics")) runCmd.PersistentFlags().String("dataplane-user-name", "haproxy", "DataplaneAPI user name") - viperBindFlag("dataplane.user.name", runCmd.PersistentFlags().Lookup("dataplane-user-name")) + viperx.MustBindFlag(viper.GetViper(), "dataplane.user.name", runCmd.PersistentFlags().Lookup("dataplane-user-name")) runCmd.PersistentFlags().String("dataplane-user-pwd", "adminpwd", "DataplaneAPI user password") - viperBindFlag("dataplane.user.pwd", runCmd.PersistentFlags().Lookup("dataplane-user-pwd")) + viperx.MustBindFlag(viper.GetViper(), "dataplane.user.pwd", runCmd.PersistentFlags().Lookup("dataplane-user-pwd")) runCmd.PersistentFlags().String("dataplane-url", "http://127.0.0.1:5555/v2/", "DataplaneAPI base url") - viperBindFlag("dataplane.url", runCmd.PersistentFlags().Lookup("dataplane-url")) + viperx.MustBindFlag(viper.GetViper(), "dataplane.url", runCmd.PersistentFlags().Lookup("dataplane-url")) runCmd.PersistentFlags().String("base-haproxy-config", "", "Base config for haproxy") - viperBindFlag("haproxy.config.base", runCmd.PersistentFlags().Lookup("base-haproxy-config")) + viperx.MustBindFlag(viper.GetViper(), "haproxy.config.base", runCmd.PersistentFlags().Lookup("base-haproxy-config")) runCmd.PersistentFlags().String("loadbalancerapi-url", "", "LoadbalancerAPI url") - viperBindFlag("loadbalancerapi.url", runCmd.PersistentFlags().Lookup("loadbalancerapi-url")) + viperx.MustBindFlag(viper.GetViper(), "loadbalancerapi.url", runCmd.PersistentFlags().Lookup("loadbalancerapi-url")) runCmd.PersistentFlags().String("loadbalancer-id", "", "Loadbalancer ID to act on event changes") - viperBindFlag("loadbalancer.id", runCmd.PersistentFlags().Lookup("loadbalancer-id")) + viperx.MustBindFlag(viper.GetViper(), "loadbalancer.id", runCmd.PersistentFlags().Lookup("loadbalancer-id")) events.MustViperFlagsForSubscriber(viper.GetViper(), runCmd.PersistentFlags()) + oauth2x.MustViperFlags(viper.GetViper(), runCmd.Flags()) } func run(cmdCtx context.Context, v *viper.Viper) error { @@ -80,7 +83,17 @@ func run(cmdCtx context.Context, v *viper.Viper) error { BaseCfgPath: viper.GetString("haproxy.config.base"), } - // init other components + // init lbapi client + if config.AppConfig.OIDC.ClientID != "" { + oauthHTTPClient := oauth2x.NewClient(ctx, oauth2x.NewClientCredentialsTokenSrc(ctx, config.AppConfig.OIDC)) + mgr.LBClient = lbapi.NewClient(viper.GetString("loadbalancerapi.url"), + lbapi.WithHTTPClient(oauthHTTPClient), + ) + } else { + mgr.LBClient = lbapi.NewClient(viper.GetString("loadbalancerapi.url")) + } + + // init events subscriber subscriber, err := pubsub.NewSubscriber( ctx, config.AppConfig.Events.Subscriber, diff --git a/go.mod b/go.mod index 992b2365..2a65d9bc 100644 --- a/go.mod +++ b/go.mod @@ -11,8 +11,9 @@ require ( github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 - go.infratographer.com/x v0.3.0 + go.infratographer.com/x v0.3.1-0.20230605180922-67c1a1e705ac go.uber.org/zap v1.24.0 + golang.org/x/oauth2 v0.8.0 ) require ( @@ -24,6 +25,7 @@ require ( github.com/gofrs/flock v0.8.1 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect + github.com/golang/protobuf v1.5.3 // indirect github.com/google/renameio v1.0.1 // indirect github.com/google/uuid v1.3.0 // indirect github.com/haproxytech/go-logger v1.1.0 // indirect @@ -63,6 +65,8 @@ require ( golang.org/x/sys v0.8.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fb424515..f11eecb1 100644 --- a/go.sum +++ b/go.sum @@ -103,7 +103,9 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -115,6 +117,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/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.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= @@ -249,8 +252,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.infratographer.com/x v0.3.0 h1:bvYxdbCBGSKASVG0Rl9sTi2LzrFwuJMBPkTu1wycyH4= -go.infratographer.com/x v0.3.0/go.mod h1:w+uknSNnOrJlHiOdDPV08e5EpU47SwWXZ/vOVWBovPA= +go.infratographer.com/x v0.3.1-0.20230605180922-67c1a1e705ac h1:5PFHsIXYJqF6ALLA39JicYzkSeLOgeF1zg3Vr68GZm8= +go.infratographer.com/x v0.3.1-0.20230605180922-67c1a1e705ac/go.mod h1:GvOhGwi/1Dp5qAQSudHUdLfFmiXzzc27KBfkH0nxnEQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -352,6 +355,7 @@ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= +golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -562,7 +566,10 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/internal/config/config.go b/internal/config/config.go index cadc61a9..f1c52b16 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,8 @@ package config import ( "go.infratographer.com/x/events" "go.infratographer.com/x/loggingx" + + "go.infratographer.com/loadbalancer-manager-haproxy/x/oauth2x" ) // EventsConfig stores the configuration for a load-balancer-api events config @@ -14,4 +16,5 @@ type EventsConfig struct { var AppConfig struct { Events EventsConfig Logging loggingx.Config + OIDC oauth2x.Config } diff --git a/internal/pubsub/subscriber.go b/internal/pubsub/subscriber.go index 53190ab4..a932d904 100644 --- a/internal/pubsub/subscriber.go +++ b/internal/pubsub/subscriber.go @@ -107,7 +107,5 @@ func (s Subscriber) listen(messages <-chan *message.Message, wg *sync.WaitGroup) // Close closes the nats connection and unsubscribes from all subscriptions func (s *Subscriber) Close() error { - // TODO - @rizzza - 6/5/2023 - once @tyler's PR is merged and released, return this - // return s.subscriber.Close() - return nil + return s.subscriber.Close() } diff --git a/pkg/lbapi/client.go b/pkg/lbapi/client.go index 174e6e5f..060723ac 100644 --- a/pkg/lbapi/client.go +++ b/pkg/lbapi/client.go @@ -15,13 +15,32 @@ type GQLClient interface { // Client creates a new lb api client against a specific endpoint type Client struct { - client GQLClient + gqlCli GQLClient + httpClient *http.Client } +// ClientOption is a function that modifies a client +type ClientOption func(*Client) + // NewClient creates a new lb api client -func NewClient(url string) *Client { - return &Client{ - client: graphql.NewClient(url, &http.Client{}), +func NewClient(url string, opts ...ClientOption) *Client { + c := &Client{ + httpClient: http.DefaultClient, + } + + for _, opt := range opts { + opt(c) + } + + c.gqlCli = graphql.NewClient(url, c.httpClient) + + return c +} + +// WithHTTPClient functional option to set the http client +func WithHTTPClient(cli *http.Client) ClientOption { + return func(c *Client) { + c.httpClient = cli } } @@ -37,7 +56,7 @@ func (c *Client) GetLoadBalancer(ctx context.Context, id string) (*GetLoadBalanc } var lb GetLoadBalancer - if err := c.client.Query(ctx, &lb, vars); err != nil { + if err := c.gqlCli.Query(ctx, &lb, vars); err != nil { return nil, err } diff --git a/pkg/lbapi/client_test.go b/pkg/lbapi/client_test.go index e3d7be1d..65ed020b 100644 --- a/pkg/lbapi/client_test.go +++ b/pkg/lbapi/client_test.go @@ -27,7 +27,7 @@ func newGQLClientMock() *mock.GQLClient { func TestGetLoadBalancer(t *testing.T) { cli := Client{ - client: newGQLClientMock(), + gqlCli: newGQLClientMock(), } lb, err := cli.GetLoadBalancer(context.Background(), "badprefix-test") diff --git a/x/oauth2x/doc.go b/x/oauth2x/doc.go new file mode 100644 index 00000000..91df5ecf --- /dev/null +++ b/x/oauth2x/doc.go @@ -0,0 +1,6 @@ +// Copyright Infratographer, Inc. and/or licensed to Infratographer, Inc. under one +// or more contributor license agreements. Licensed under the Elastic License 2.0; +// you may not use this file except in compliance with the Elastic License 2.0. + +// Package oauth2x provides shared functions for setting up an oauth2 client configuration +package oauth2x diff --git a/x/oauth2x/oauth2.go b/x/oauth2x/oauth2.go new file mode 100644 index 00000000..e5a39bc8 --- /dev/null +++ b/x/oauth2x/oauth2.go @@ -0,0 +1,50 @@ +package oauth2x + +import ( + "context" + "net/http" + + "github.com/spf13/pflag" + "github.com/spf13/viper" + + "go.infratographer.com/x/viperx" + + "golang.org/x/oauth2" + "golang.org/x/oauth2/clientcredentials" +) + +// NewClientCredentialsTokenSrc returns an oauth2 client credentials token source +func NewClientCredentialsTokenSrc(ctx context.Context, cfg Config) oauth2.TokenSource { + ccCfg := clientcredentials.Config{ + ClientID: cfg.ClientID, + ClientSecret: cfg.ClientSecret, + TokenURL: cfg.TokenURL, + } + + return ccCfg.TokenSource(ctx) +} + +// NewClient returns a http client using requested token source +func NewClient(ctx context.Context, tokenSrc oauth2.TokenSource) *http.Client { + return oauth2.NewClient(ctx, tokenSrc) +} + +// Config handles reading in all the config values available +// for setting up an oauth2 configuration +type Config struct { + ClientID string `mapstructure:"id"` + ClientSecret string `mapstructure:"secret"` + TokenURL string `mapstructure:"tokenURL"` +} + +// MustViperFlags adds oidc oauth2 client credentials config to the provided flagset and binds to viper +func MustViperFlags(v *viper.Viper, flags *pflag.FlagSet) { + flags.String("oidc-client-id", "", "expected oidc client identifier") + viperx.MustBindFlag(v, "oidc.client.id", flags.Lookup("oidc-client-id")) + + flags.String("oidc-client-secret", "", "expected oidc client secret") + viperx.MustBindFlag(v, "oidc.client.secret", flags.Lookup("oidc-client-secret")) + + flags.String("oidc-client-token-url", "", "expected oidc token url") + viperx.MustBindFlag(v, "oidc.client.tokenURL", flags.Lookup("oidc-client-token-url")) +}