| 1 | package hostenv |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "errors" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/aws/aws-sdk-go-v2/aws" |
| 9 | "github.com/aws/aws-sdk-go-v2/config" |
| 10 | "github.com/aws/aws-sdk-go-v2/service/ssm" |
| 11 | ssmtypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" |
| 12 | ) |
| 13 | |
| 14 | // The AWS adapter for Params (region HA spec 2 §25): SSM Parameter Store |
| 15 | // SecureStrings, encrypted with the deployment's configuration key (§4). |
| 16 | // A box reads its two paths with the role's ssm:GetParametersByPath and |
| 17 | // kms:Decrypt; an operator seeding them needs ssm:PutParameter and |
| 18 | // kms:Encrypt on that key. |
| 19 | |
| 20 | // ssmParams holds method values bound to the two SSM calls it makes, not |
| 21 | // the *ssm.Client. Handing the client to the paginator (whose argument is |
| 22 | // an interface) would make the linker keep every exported SSM method once |
| 23 | // html/template is linked in: about 5 MB of linux/arm64 binary (discussion |
| 24 | // #275, .amadan/ci.d/20-cross@5). A method value keeps the one call alone. |
| 25 | type ssmParams struct { |
| 26 | getByPath getParametersByPathFunc |
| 27 | put func(context.Context, *ssm.PutParameterInput, ...func(*ssm.Options)) (*ssm.PutParameterOutput, error) |
| 28 | kmsKey string |
| 29 | } |
| 30 | |
| 31 | // getParametersByPathFunc is ssm.GetParametersByPathAPIClient over one |
| 32 | // bound method value, for the paginator. |
| 33 | type getParametersByPathFunc func(context.Context, *ssm.GetParametersByPathInput, ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) |
| 34 | |
| 35 | func (f getParametersByPathFunc) GetParametersByPath(ctx context.Context, in *ssm.GetParametersByPathInput, opts ...func(*ssm.Options)) (*ssm.GetParametersByPathOutput, error) { |
| 36 | return f(ctx, in, opts...) |
| 37 | } |
| 38 | |
| 39 | // openSSMParams opens SSM in awsRegion ("" is the SDK's default region). |
| 40 | // kmsKey is the key Set encrypts with. |
| 41 | func openSSMParams(ctx context.Context, awsRegion, kmsKey string) (Params, error) { |
| 42 | var opts []func(*config.LoadOptions) error |
| 43 | if awsRegion != "" { |
| 44 | opts = append(opts, config.WithRegion(awsRegion)) |
| 45 | } |
| 46 | cfg, err := config.LoadDefaultConfig(ctx, opts...) |
| 47 | if err != nil { |
| 48 | return nil, err |
| 49 | } |
| 50 | client := ssm.NewFromConfig(cfg) |
| 51 | return &ssmParams{ |
| 52 | getByPath: getParametersByPathFunc(client.GetParametersByPath), |
| 53 | put: client.PutParameter, |
| 54 | kmsKey: kmsKey, |
| 55 | }, nil |
| 56 | } |
| 57 | |
| 58 | // ByPath reads every parameter one level under prefix, decrypted, page by |
| 59 | // page. Any failed page fails the whole read. |
| 60 | func (p *ssmParams) ByPath(ctx context.Context, prefix string) (map[string]string, error) { |
| 61 | out := map[string]string{} |
| 62 | pages := ssm.NewGetParametersByPathPaginator(p.getByPath, &ssm.GetParametersByPathInput{ |
| 63 | Path: aws.String(strings.TrimSuffix(prefix, "/")), |
| 64 | Recursive: aws.Bool(false), |
| 65 | WithDecryption: aws.Bool(true), |
| 66 | }) |
| 67 | for pages.HasMorePages() { |
| 68 | page, err := pages.NextPage(ctx) |
| 69 | if err != nil { |
| 70 | return nil, err |
| 71 | } |
| 72 | for _, param := range page.Parameters { |
| 73 | name, ok := strings.CutPrefix(aws.ToString(param.Name), prefix) |
| 74 | if !ok || name == "" || strings.Contains(name, "/") { |
| 75 | continue |
| 76 | } |
| 77 | out[name] = aws.ToString(param.Value) |
| 78 | } |
| 79 | } |
| 80 | return out, nil |
| 81 | } |
| 82 | |
| 83 | // Set writes one SecureString with the deployment's key, replacing it: |
| 84 | // a region's own values. |
| 85 | func (p *ssmParams) Set(ctx context.Context, name, value string) error { |
| 86 | in := &ssm.PutParameterInput{ |
| 87 | Name: aws.String(name), |
| 88 | Value: aws.String(value), |
| 89 | Type: ssmtypes.ParameterTypeSecureString, |
| 90 | Overwrite: aws.Bool(true), |
| 91 | } |
| 92 | if p.kmsKey != "" { |
| 93 | in.KeyId = aws.String(p.kmsKey) |
| 94 | } |
| 95 | _, err := p.put(ctx, in) |
| 96 | return err |
| 97 | } |
| 98 | |
| 99 | // SetNew writes one SecureString with the deployment's key only if it does |
| 100 | // not exist (Overwrite false). SSM's ParameterAlreadyExists is ErrExists. |
| 101 | func (p *ssmParams) SetNew(ctx context.Context, name, value string) error { |
| 102 | in := &ssm.PutParameterInput{ |
| 103 | Name: aws.String(name), |
| 104 | Value: aws.String(value), |
| 105 | Type: ssmtypes.ParameterTypeSecureString, |
| 106 | Overwrite: aws.Bool(false), |
| 107 | } |
| 108 | if p.kmsKey != "" { |
| 109 | in.KeyId = aws.String(p.kmsKey) |
| 110 | } |
| 111 | _, err := p.put(ctx, in) |
| 112 | var exists *ssmtypes.ParameterAlreadyExists |
| 113 | if errors.As(err, &exists) { |
| 114 | return ErrExists |
| 115 | } |
| 116 | return err |
| 117 | } |
| 118 | |