ipn/store/aws, cmd/tailscaled: add AWS SSM ipn.StateStore implementation
From https://github.com/tailscale/tailscale/pull/1919 with edits by bradfitz@. This change introduces a new storage provider for the state file. It allows users to leverage AWS SSM parameter store natively within tailscaled, like: $ tailscaled --state=arn:aws:ssm:eu-west-1:123456789:parameter/foo Known limitations: - it is not currently possible to specific a custom KMS key ID RELNOTE=tailscaled on Linux supports using AWS SSM for state Edits-By: Brad Fitzpatrick <bradfitz@tailscale.com> Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com> Signed-off-by: Maxime VISONNEAU <maxime.visonneau@gmail.com>pull/3061/head
parent
1b20d1ce54
commit
4528f448d6
|
@ -3,6 +3,61 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||||
|
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/aws
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/protocol/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/ratelimit from github.com/aws/aws-sdk-go-v2/aws/retry
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/retry from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4 from github.com/aws/aws-sdk-go-v2/aws/signer/v4
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/signer/v4 from github.com/aws/aws-sdk-go-v2/service/internal/presigned-url+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/aws/transport/http from github.com/aws/aws-sdk-go-v2/config+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/config from tailscale.com/ipn/store/aws
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials/endpointcreds/internal/client from github.com/aws/aws-sdk-go-v2/credentials/endpointcreds
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials/processcreds from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials/ssocreds from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
|
||||||
|
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/internal/presigned-url from github.com/aws/aws-sdk-go-v2/service/sts
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/ssm from tailscale.com/ipn/store/aws
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/ssm/types from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
|
||||||
|
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
|
||||||
|
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
|
||||||
|
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||||
|
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
|
||||||
|
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||||
|
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||||
|
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||||
|
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||||
|
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
|
||||||
|
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
|
||||||
|
L github.com/aws/smithy-go/ptr from github.com/aws/aws-sdk-go-v2/aws+
|
||||||
|
L github.com/aws/smithy-go/rand from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||||
|
L github.com/aws/smithy-go/time from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||||
|
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||||
|
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
|
||||||
|
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||||
github.com/go-multierror/multierror from tailscale.com/wgengine/router+
|
github.com/go-multierror/multierror from tailscale.com/wgengine/router+
|
||||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||||
|
@ -13,6 +68,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||||
|
L github.com/jmespath/go-jmespath from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||||
|
@ -106,6 +162,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||||
|
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
|
||||||
tailscale.com/kube from tailscale.com/ipn
|
tailscale.com/kube from tailscale.com/ipn
|
||||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||||
|
|
|
@ -114,7 +114,7 @@ func main() {
|
||||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:<secret-name>' to use Kubernetes secrets")
|
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM")
|
||||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -7,6 +7,9 @@ require (
|
||||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/aws/aws-sdk-go v1.38.52
|
github.com/aws/aws-sdk-go v1.38.52
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.9.2
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.8.3
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0
|
||||||
github.com/coreos/go-iptables v0.6.0
|
github.com/coreos/go-iptables v0.6.0
|
||||||
github.com/creack/pty v1.1.16
|
github.com/creack/pty v1.1.16
|
||||||
github.com/dave/jennifer v1.4.1
|
github.com/dave/jennifer v1.4.1
|
||||||
|
@ -66,6 +69,13 @@ require (
|
||||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||||
github.com/Microsoft/go-winio v0.4.16 // indirect
|
github.com/Microsoft/go-winio v0.4.16 // indirect
|
||||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 // indirect
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 // indirect
|
||||||
|
github.com/aws/smithy-go v1.8.0 // indirect
|
||||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||||
github.com/bombsimon/wsl/v3 v3.1.0 // indirect
|
github.com/bombsimon/wsl/v3 v3.1.0 // indirect
|
||||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
||||||
|
|
20
go.sum
20
go.sum
|
@ -59,6 +59,26 @@ 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/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/aws/aws-sdk-go v1.38.52 h1:7NKcUyTG/CyDX835kq04DDNe8vXaJhbGW8ThemHb18A=
|
github.com/aws/aws-sdk-go v1.38.52 h1:7NKcUyTG/CyDX835kq04DDNe8vXaJhbGW8ThemHb18A=
|
||||||
github.com/aws/aws-sdk-go v1.38.52/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
github.com/aws/aws-sdk-go v1.38.52/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.9.2 h1:dUFQcMNZMLON4BOe273pl0filK9RqyQMhCK/6xssL6s=
|
||||||
|
github.com/aws/aws-sdk-go-v2 v1.9.2/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.8.3 h1:o5583X4qUfuRrOGOgmOcDgvr5gJVSu57NK08cWAhIDk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/config v1.8.3/go.mod h1:4AEiLtAb8kLs7vgw2ZV3p2VZ1+hBavOc84hqxVNpCyw=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 h1:LTdD5QhK073MpElh9umLLP97wxphkgVC/OjQaEbBwZA=
|
||||||
|
github.com/aws/aws-sdk-go-v2/credentials v1.4.3/go.mod h1:FNNC6nQZQUuyhq5aE5c7ata8o9e4ECGmS4lAXC7o1mQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 h1:9tfxW/icbSu98C2pcNynm5jmDwU3/741F11688B6QnU=
|
||||||
|
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0/go.mod h1:gqlclDEZp4aqJOancXK6TN24aKhT0W0Ae9MHk3wzTMM=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 h1:leSJ6vCqtPpTmBIgE7044B1wql1E4n//McF+mEgNrYg=
|
||||||
|
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4/go.mod h1:ZcBrrI3zBKlhGFNYWvju0I3TR93I7YIgAfy82Fh4lcQ=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 h1:r7jel2aa4d9Duys7wEmWqDd5ebpC9w6Kxu6wIjjp18E=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2/go.mod h1:72HRZDLMtmVQiLG2tLfQcaWLCssELvGl+Zf2WVxMmR8=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0 h1:zJfVytawApwhwjDq3tbuzuLjNKQvrhdPM+II2MQDRTI=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0/go.mod h1:m3cb1hedrft0oYmueH0CkBgRdiwczuKRXPr0tilSpz4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 h1:pZwkxZbspdqRGzddDB92bkZBoB7lg85sMRE7OqdB3V0=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2/go.mod h1:NBvT9R1MEF+Ud6ApJKM0G+IkPchKS7p7c2YPKwHmBOk=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 h1:ol2Y5DWqnJeKqNd8th7JWzBtqu63xpOfs1Is+n1t8/4=
|
||||||
|
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2/go.mod h1:8EzeIqfWt2wWT4rJVu3f21TfrhJ8AEMzVybRNSb/b4g=
|
||||||
|
github.com/aws/smithy-go v1.8.0 h1:AEwwwXQZtUwP5Mz506FeXXrKBe0jA8gVM+1gEcSRooc=
|
||||||
|
github.com/aws/smithy-go v1.8.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
|
||||||
|
|
|
@ -35,6 +35,7 @@ import (
|
||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
"tailscale.com/ipn/localapi"
|
"tailscale.com/ipn/localapi"
|
||||||
|
"tailscale.com/ipn/store/aws"
|
||||||
"tailscale.com/log/filelogger"
|
"tailscale.com/log/filelogger"
|
||||||
"tailscale.com/logtail/backoff"
|
"tailscale.com/logtail/backoff"
|
||||||
"tailscale.com/net/netstat"
|
"tailscale.com/net/netstat"
|
||||||
|
@ -638,6 +639,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||||
var store ipn.StateStore
|
var store ipn.StateStore
|
||||||
if opts.StatePath != "" {
|
if opts.StatePath != "" {
|
||||||
const kubePrefix = "kube:"
|
const kubePrefix = "kube:"
|
||||||
|
const arnPrefix = "arn:"
|
||||||
path := opts.StatePath
|
path := opts.StatePath
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(path, kubePrefix):
|
case strings.HasPrefix(path, kubePrefix):
|
||||||
|
@ -646,6 +648,11 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err)
|
return fmt.Errorf("ipn.NewKubeStore(%q): %v", secretName, err)
|
||||||
}
|
}
|
||||||
|
case strings.HasPrefix(path, arnPrefix):
|
||||||
|
store, err = aws.NewStore(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("aws.NewStore(%q): %v", path, err)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
path = tryWindowsAppDataMigration(logf, path)
|
path = tryWindowsAppDataMigration(logf, path)
|
||||||
|
|
20
ipn/store.go
20
ipn/store.go
|
@ -158,6 +158,26 @@ func (s *MemoryStore) WriteState(id StateKey, bs []byte) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadFromJSON attempts to unmarshal json content into the
|
||||||
|
// in-memory cache.
|
||||||
|
func (s *MemoryStore) LoadFromJSON(data []byte) error {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return json.Unmarshal(data, &s.cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExportToJSON exports the content of the cache to
|
||||||
|
// JSON formatted []byte.
|
||||||
|
func (s *MemoryStore) ExportToJSON() ([]byte, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if len(s.cache) == 0 {
|
||||||
|
// Avoid "null" serialization.
|
||||||
|
return []byte("{}"), nil
|
||||||
|
}
|
||||||
|
return json.MarshalIndent(s.cache, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
// FileStore is a StateStore that uses a JSON file for persistence.
|
// FileStore is a StateStore that uses a JSON file for persistence.
|
||||||
type FileStore struct {
|
type FileStore struct {
|
||||||
path string
|
path string
|
||||||
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
// Package aws contains an AWS SSM StateStore implementation.
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||||
|
ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
parameterNameRxStr = `^parameter/(.*)`
|
||||||
|
)
|
||||||
|
|
||||||
|
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
|
||||||
|
|
||||||
|
// awsSSMClient is an interface allowing us to mock the couple of
|
||||||
|
// API calls we are leveraging with the AWSStore provider
|
||||||
|
type awsSSMClient interface {
|
||||||
|
GetParameter(ctx context.Context,
|
||||||
|
params *ssm.GetParameterInput,
|
||||||
|
optFns ...func(*ssm.Options)) (*ssm.GetParameterOutput, error)
|
||||||
|
|
||||||
|
PutParameter(ctx context.Context,
|
||||||
|
params *ssm.PutParameterInput,
|
||||||
|
optFns ...func(*ssm.Options)) (*ssm.PutParameterOutput, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// store is a store which leverages AWS SSM parameter store
|
||||||
|
// to persist the state
|
||||||
|
type awsStore struct {
|
||||||
|
ssmClient awsSSMClient
|
||||||
|
ssmARN arn.ARN
|
||||||
|
|
||||||
|
memory ipn.MemoryStore
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStore returns a new ipn.StateStore using the AWS SSM storage
|
||||||
|
// location given by ssmARN.
|
||||||
|
func NewStore(ssmARN string) (ipn.StateStore, error) {
|
||||||
|
return newStore(ssmARN, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStore is NewStore, but for tests. If client is non-nil, it's
|
||||||
|
// used instead of making one.
|
||||||
|
func newStore(ssmARN string, client awsSSMClient) (ipn.StateStore, error) {
|
||||||
|
s := &awsStore{
|
||||||
|
ssmClient: client,
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// Parse the ARN
|
||||||
|
if s.ssmARN, err = arn.Parse(ssmARN); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse the ARN correctly: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the ARN corresponds to the SSM service
|
||||||
|
if s.ssmARN.Service != "ssm" {
|
||||||
|
return nil, fmt.Errorf("invalid service %q, expected 'ssm'", s.ssmARN.Service)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate the ARN corresponds to a parameter store resource
|
||||||
|
if !parameterNameRx.MatchString(s.ssmARN.Resource) {
|
||||||
|
return nil, fmt.Errorf("invalid resource %q, expected to match %v", s.ssmARN.Resource, parameterNameRxStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ssmClient == nil {
|
||||||
|
var cfg aws.Config
|
||||||
|
if cfg, err = config.LoadDefaultConfig(
|
||||||
|
context.TODO(),
|
||||||
|
config.WithRegion(s.ssmARN.Region),
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.ssmClient = ssm.NewFromConfig(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hydrate cache with the potentially current state
|
||||||
|
if err := s.LoadState(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadState attempts to read the state from AWS SSM parameter store key.
|
||||||
|
func (s *awsStore) LoadState() error {
|
||||||
|
param, err := s.ssmClient.GetParameter(
|
||||||
|
context.TODO(),
|
||||||
|
&ssm.GetParameterInput{
|
||||||
|
Name: aws.String(s.ParameterName()),
|
||||||
|
WithDecryption: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
var pnf *ssmTypes.ParameterNotFound
|
||||||
|
if errors.As(err, &pnf) {
|
||||||
|
// Create the parameter as it does not exist yet
|
||||||
|
// and return directly as it is defacto empty
|
||||||
|
return s.persistState()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the content in-memory
|
||||||
|
return s.memory.LoadFromJSON([]byte(*param.Parameter.Value))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterName returns the parameter name extracted from
|
||||||
|
// the provided ARN
|
||||||
|
func (s *awsStore) ParameterName() (name string) {
|
||||||
|
values := parameterNameRx.FindStringSubmatch(s.ssmARN.Resource)
|
||||||
|
if len(values) == 2 {
|
||||||
|
name = values[1]
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the awsStore and the ARN of the SSM parameter store
|
||||||
|
// configured to store the state
|
||||||
|
func (s *awsStore) String() string { return fmt.Sprintf("awsStore(%q)", s.ssmARN.String()) }
|
||||||
|
|
||||||
|
// ReadState implements the Store interface.
|
||||||
|
func (s *awsStore) ReadState(id ipn.StateKey) (bs []byte, err error) {
|
||||||
|
return s.memory.ReadState(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteState implements the Store interface.
|
||||||
|
func (s *awsStore) WriteState(id ipn.StateKey, bs []byte) (err error) {
|
||||||
|
// Write the state in-memory
|
||||||
|
if err = s.memory.WriteState(id, bs); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Persist the state in AWS SSM parameter store
|
||||||
|
return s.persistState()
|
||||||
|
}
|
||||||
|
|
||||||
|
// PersistState saves the states into the AWS SSM parameter store
|
||||||
|
func (s *awsStore) persistState() error {
|
||||||
|
// Generate JSON from in-memory cache
|
||||||
|
bs, err := s.memory.ExportToJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store in AWS SSM parameter store
|
||||||
|
_, err = s.ssmClient.PutParameter(
|
||||||
|
context.TODO(),
|
||||||
|
&ssm.PutParameterInput{
|
||||||
|
Name: aws.String(s.ParameterName()),
|
||||||
|
Value: aws.String(string(bs)),
|
||||||
|
Overwrite: true,
|
||||||
|
Tier: ssmTypes.ParameterTierStandard,
|
||||||
|
Type: ssmTypes.ParameterTypeSecureString,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build !linux
|
||||||
|
// +build !linux
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewStore(string) (ipn.StateStore, error) {
|
||||||
|
return nil, fmt.Errorf("AWS store is not supported on %v", runtime.GOOS)
|
||||||
|
}
|
|
@ -0,0 +1,166 @@
|
||||||
|
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
//go:build linux
|
||||||
|
// +build linux
|
||||||
|
|
||||||
|
package aws
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||||
|
"github.com/aws/aws-sdk-go-v2/service/ssm"
|
||||||
|
ssmTypes "github.com/aws/aws-sdk-go-v2/service/ssm/types"
|
||||||
|
"tailscale.com/ipn"
|
||||||
|
"tailscale.com/tstest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type mockedAWSSSMClient struct {
|
||||||
|
value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *mockedAWSSSMClient) GetParameter(_ context.Context, input *ssm.GetParameterInput, _ ...func(*ssm.Options)) (*ssm.GetParameterOutput, error) {
|
||||||
|
output := new(ssm.GetParameterOutput)
|
||||||
|
if sp.value == "" {
|
||||||
|
return output, &ssmTypes.ParameterNotFound{}
|
||||||
|
}
|
||||||
|
|
||||||
|
output.Parameter = &ssmTypes.Parameter{
|
||||||
|
Value: aws.String(sp.value),
|
||||||
|
}
|
||||||
|
|
||||||
|
return output, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sp *mockedAWSSSMClient) PutParameter(_ context.Context, input *ssm.PutParameterInput, _ ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) {
|
||||||
|
sp.value = *input.Value
|
||||||
|
return new(ssm.PutParameterOutput), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAWSStoreString(t *testing.T) {
|
||||||
|
store := &awsStore{
|
||||||
|
ssmARN: arn.ARN{
|
||||||
|
Service: "ssm",
|
||||||
|
Region: "eu-west-1",
|
||||||
|
AccountID: "123456789",
|
||||||
|
Resource: "parameter/foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
want := "awsStore(\"arn::ssm:eu-west-1:123456789:parameter/foo\")"
|
||||||
|
if got := store.String(); got != want {
|
||||||
|
t.Errorf("AWSStore.String = %q; want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewAWSStore(t *testing.T) {
|
||||||
|
tstest.PanicOnLog()
|
||||||
|
|
||||||
|
mc := &mockedAWSSSMClient{}
|
||||||
|
storeParameterARN := arn.ARN{
|
||||||
|
Service: "ssm",
|
||||||
|
Region: "eu-west-1",
|
||||||
|
AccountID: "123456789",
|
||||||
|
Resource: "parameter/foo",
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := newStore(storeParameterARN.String(), mc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating aws store failed: %v", err)
|
||||||
|
}
|
||||||
|
testStoreSemantics(t, s)
|
||||||
|
|
||||||
|
// Build a brand new file store and check that both IDs written
|
||||||
|
// above are still there.
|
||||||
|
s2, err := newStore(storeParameterARN.String(), mc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("creating second aws store failed: %v", err)
|
||||||
|
}
|
||||||
|
store2 := s.(*awsStore)
|
||||||
|
|
||||||
|
// This is specific to the test, with the non-mocked API, LoadState() should
|
||||||
|
// have been already called and sucessful as no err is returned from NewAWSStore()
|
||||||
|
s2.(*awsStore).LoadState()
|
||||||
|
|
||||||
|
expected := map[ipn.StateKey]string{
|
||||||
|
"foo": "bar",
|
||||||
|
"baz": "quux",
|
||||||
|
}
|
||||||
|
for id, want := range expected {
|
||||||
|
bs, err := store2.ReadState(id)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("reading %q (2nd store): %v", id, err)
|
||||||
|
}
|
||||||
|
if string(bs) != want {
|
||||||
|
t.Errorf("reading %q (2nd store): got %q, want %q", id, string(bs), want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStoreSemantics(t *testing.T, store ipn.StateStore) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
// if true, data is data to write. If false, data is expected
|
||||||
|
// output of read.
|
||||||
|
write bool
|
||||||
|
id ipn.StateKey
|
||||||
|
data string
|
||||||
|
// If write=false, true if we expect a not-exist error.
|
||||||
|
notExists bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
notExists: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
write: true,
|
||||||
|
id: "foo",
|
||||||
|
data: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
data: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "baz",
|
||||||
|
notExists: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
write: true,
|
||||||
|
id: "baz",
|
||||||
|
data: "quux",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "foo",
|
||||||
|
data: "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "baz",
|
||||||
|
data: "quux",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
if test.write {
|
||||||
|
if err := store.WriteState(test.id, []byte(test.data)); err != nil {
|
||||||
|
t.Errorf("writing %q to %q: %v", test.data, test.id, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bs, err := store.ReadState(test.id)
|
||||||
|
if err != nil {
|
||||||
|
if test.notExists && err == ipn.ErrStateNotExist {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("reading %q: %v", test.id, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if string(bs) != test.data {
|
||||||
|
t.Errorf("reading %q: got %q, want %q", test.id, string(bs), test.data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue