mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 13:05:46 +00:00
6793685bba
They changed a type in their SDK which meant others using the AWS APIs in their Go programs (with newer AWS modules in their caller go.mod) and then depending on Tailscale (for e.g. tsnet) then couldn't compile ipn/store/awsstore. Thanks to @thisisaaronland for bringing this up. Fixes #7019 Change-Id: I8d2919183dabd6045a96120bb52940a9bb27193b Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
177 lines
4.6 KiB
Go
177 lines
4.6 KiB
Go
// 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 && !ts_omit_aws
|
|
|
|
// Package awsstore contains an ipn.StateStore implementation using AWS SSM.
|
|
package awsstore
|
|
|
|
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"
|
|
"tailscale.com/ipn/store/mem"
|
|
"tailscale.com/types/logger"
|
|
)
|
|
|
|
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 mem.Store
|
|
}
|
|
|
|
// New returns a new ipn.StateStore using the AWS SSM storage
|
|
// location given by ssmARN.
|
|
func New(_ logger.Logf, 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: aws.Bool(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: aws.Bool(true),
|
|
Tier: ssmTypes.ParameterTierStandard,
|
|
Type: ssmTypes.ParameterTypeSecureString,
|
|
},
|
|
)
|
|
return err
|
|
}
|