profile
viewpoint
Clint catsby @hashicorp Missouri catsby.net I work at HashiCorp, where I engineer things

catsby/ascii 34

Awesome ascii

catsby/CocoaREST 26

Cocoa classes to interact with RESTful services (including Twitter)

catsby/capistrano-chef 9

Capistrano extensions for Chef integration

catsby/ctshryock.com 3

Source for ctshryock.com, my personal site

catsby/deptrack 3

Experimental tool to gather names, versions, and revisions of all our vendored dependencies

catsby/Chameleon 1

Chameleon is a port of Apple's UIKit for iOS (and some minimal related frameworks) to Mac OS X.

catsby/classifier 1

A general classifier module to allow Bayesian and other types of classifications.

catsby/college-football-widget 1

Mac Dashboard Widget for college football schedules

pull request commentterraform-providers/terraform-provider-vault

Feature: Codegen and support for transform endpoints

Merging this in, thanks @tyrannosaurus-becks !

tyrannosaurus-becks

comment created time in 5 hours

push eventterraform-providers/terraform-provider-vault

Becca Petrin

commit sha c4d5ee7807f0a9b5516e6505a18cfe8bbddac8c6

Feature: Codegen and support for transform endpoints (#783) * Move provider to separate package (#737) * move provider to separate package * rename provider package to schema * Add directory structure (#738) * add make dev command * add code for path structure * add trailing slash to mv * Update codegen/generate.go Co-Authored-By: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update codegen/generate.go Co-Authored-By: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update codegen/generate.go Co-Authored-By: Michael Golowka <72365+pcman312@users.noreply.github.com> * add unset template type * resolve naming clash to fix tests * refactor test to use testcases rather than indexes * strip dev command because its redundant Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * 1. Add code generation for a resource (#746) * add code generation for a resource * Update cmd/generate/main.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update cmd/generate/main.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update cmd/generate/main.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update codegen/generate.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * incorporate feedback * Update codegen/templates_test.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update codegen/templates_test.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update codegen/templates.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update codegen/generate_test.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * changes from feedback * update tests * update var name * return err from parsing home dir * put test in t.Run * use errwrap * use filepath.Dir * more changes from feedback * more feedback * break parameter validation into its on method * more feedback * test table for Validate * use stringbuilder * rename file * expand upon path description * return err * expand upon err * add tests for path param func * strip unused test Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Remaining code generation and endpoints (#749) * add code generation for a resource * generate /transform/transformation/{name} * 3. Add a generated data source (#750) * add a generated data source * add data source to endpoint registry * 4. Generate docs (#754) * generate docs * improve example hcl * 5. Add a /transform/alphabet/{name} resource (#755) * add a /transform/alphabet/{name} resource * 6. Add a /transform/encode/{role_name} data source (#756) * add a /transform/encode/{role_name} data source * 7. Add a resource for /transform/template/{name} (#757) * add a resource for /transform/template/{name} * strip TODO * improve README * run gen and fmt at once * improve script output * add tfType * encapsulate parameter grooming * increase test for format * add TestEntPreCheck * avoid pointers to templatable params * fix os.Exits from moving code * add resource that supports importing jwt config (#780) * Remove _name suffix * remove the jwt/config resource from the code generated files (#807) * remove the jwt/config resource from the code generated files * remove jwt/config from the registry * update documentation to remove _name, jwt config generated docs * update docs to remove _name and -name suffix * update links * update link src * doc format update * update docs to fix formatting * update template docs and add template to sidebar Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> Co-authored-by: Clint <catsby@users.noreply.github.com> Co-authored-by: catsby <clint@ctshryock.com>

view details

push time in 5 hours

PR merged terraform-providers/terraform-provider-vault

Reviewers
Feature: Codegen and support for transform endpoints size/XXL

This PR adds support for a nascent code generation functionality. The intent of code generation is to speed up the development process, and to provide more standardized (and hopefully less buggy) code. As it's used for more resources and data sources, templates and logic will likely need to be tweaked to accommodate them. The codegen package includes a README.md describing how to use it properly.

This PR also adds support for the following generated endpoints:

  • ~"/auth/jwt/config" as a resource~ Update from @catsby 2020-07-01: This duplicated the existing resource_jwt_auth and was removed in https://github.com/terraform-providers/terraform-provider-vault/pull/807
  • "/transform/alphabet/{name}" as a resource
  • "/transform/decode/{role_name}" as a data source
  • "/transform/encode/{role_name}" as a data source
  • "/transform/role/{name}" as a resource
  • "/transform/template/{name}" as a resource
  • "/transform/transformation/{name}" as a resource

Since this is a large PR, it's likely there will be a large number of comments/changes on the code. For those, please either 1) PR or commit them directly into this feature branch to add them to the present PR, or 2) close this PR, add more to the branch, and re-PR.

Thank you!

<details> <summary>Related test output</summary>

=== RUN   TestCodeFilePath
--- PASS: TestCodeFilePath (0.00s)
=== RUN   TestDocFilePath
--- PASS: TestDocFilePath (0.00s)
=== RUN   TestStripCurlyBraces
=== RUN   TestStripCurlyBraces/{test}
=== RUN   TestStripCurlyBraces/{{name}}
=== RUN   TestStripCurlyBraces/name
=== RUN   TestStripCurlyBraces/{name
--- PASS: TestStripCurlyBraces (0.00s)
    --- PASS: TestStripCurlyBraces/{test} (0.00s)
    --- PASS: TestStripCurlyBraces/{{name}} (0.00s)
    --- PASS: TestStripCurlyBraces/name (0.00s)
    --- PASS: TestStripCurlyBraces/{name (0.00s)
=== RUN   TestFormat
=== RUN   TestFormat/alphabet
=== RUN   TestFormat/{name}
=== RUN   TestFormat/{role_name}
=== RUN   TestFormat/{name}#01
=== RUN   TestFormat/{version}
=== RUN   TestFormat/unlikely
=== RUN   TestFormat/{role_name_}
=== RUN   TestFormat/{role__name}
=== RUN   TestFormat/{role_name_here}
=== RUN   TestFormat/{rOlE_nAmE}
=== RUN   TestFormat/{ROLE_NAME}
--- PASS: TestFormat (0.00s)
    --- PASS: TestFormat/alphabet (0.00s)
    --- PASS: TestFormat/{name} (0.00s)
    --- PASS: TestFormat/{role_name} (0.00s)
    --- PASS: TestFormat/{name}#01 (0.00s)
    --- PASS: TestFormat/{version} (0.00s)
    --- PASS: TestFormat/unlikely (0.00s)
    --- PASS: TestFormat/{role_name_} (0.00s)
    --- PASS: TestFormat/{role__name} (0.00s)
    --- PASS: TestFormat/{role_name_here} (0.00s)
    --- PASS: TestFormat/{rOlE_nAmE} (0.00s)
    --- PASS: TestFormat/{ROLE_NAME} (0.00s)
=== RUN   TestValidate
=== RUN   TestValidate/nil_inputs_error
=== RUN   TestValidate/blank_endpoints_error
=== RUN   TestValidate/blank_dirnames_error
=== RUN   TestValidate/blank_upper_case_differentiators_error
=== RUN   TestValidate/blank_lower_case_differentiators_error
=== RUN   TestValidate/valid_endpoint
=== RUN   TestValidate/bad_parameter_type
=== RUN   TestValidate/good_parameter_type
=== RUN   TestValidate/array_of_strings_param
=== RUN   TestValidate/array_of_objects_param
--- PASS: TestValidate (0.00s)
    --- PASS: TestValidate/nil_inputs_error (0.00s)
    --- PASS: TestValidate/blank_endpoints_error (0.00s)
    --- PASS: TestValidate/blank_dirnames_error (0.00s)
    --- PASS: TestValidate/blank_upper_case_differentiators_error (0.00s)
    --- PASS: TestValidate/blank_lower_case_differentiators_error (0.00s)
    --- PASS: TestValidate/valid_endpoint (0.00s)
    --- PASS: TestValidate/bad_parameter_type (0.00s)
    --- PASS: TestValidate/good_parameter_type (0.00s)
    --- PASS: TestValidate/array_of_strings_param (0.00s)
    --- PASS: TestValidate/array_of_objects_param (0.00s)
=== RUN   TestToTemplatableParam
--- PASS: TestToTemplatableParam (0.00s)
=== RUN   TestParseParameters
=== RUN   TestParseParameters//transform/role/{name}
=== RUN   TestParseParameters//transform/alphabet/{name}
--- PASS: TestParseParameters (0.00s)
    --- PASS: TestParseParameters//transform/role/{name} (0.00s)
    --- PASS: TestParseParameters//transform/alphabet/{name} (0.00s)
=== RUN   TestTemplateHandler
--- PASS: TestTemplateHandler (0.00s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/codegen	(cached)
?   	github.com/terraform-providers/terraform-provider-vault/generated	[no test files]
=== RUN   TestDecodeBasic
--- PASS: TestDecodeBasic (0.26s)
=== RUN   TestDecodeBatch
--- PASS: TestDecodeBatch (0.30s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/datasources/transform/decode	0.580s
=== RUN   TestEncodeBasic
--- PASS: TestEncodeBasic (0.27s)
=== RUN   TestEncodeBatch
--- PASS: TestEncodeBatch (0.30s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/datasources/transform/encode	0.594s
=== RUN   TestConfig
--- PASS: TestConfig (0.79s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/resources/auth/jwt	0.813s
=== RUN   TestAlphabetName
--- PASS: TestAlphabetName (0.59s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/resources/transform/alphabet	0.608s
=== RUN   TestRoleName
--- PASS: TestRoleName (0.59s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/resources/transform/role	0.614s
=== RUN   TestTemplateName
--- PASS: TestTemplateName (0.48s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/resources/transform/template	0.690s
=== RUN   TestTransformationName
--- PASS: TestTransformationName (0.38s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/generated/resources/transform/transformation	0.396s
=== RUN   TestParsePath
=== RUN   TestParsePath/my/transform/hello
=== RUN   TestParsePath/jwt-1914071788362821795
=== RUN   TestParsePath/accounting-transit
--- PASS: TestParsePath (0.00s)
    --- PASS: TestParsePath/my/transform/hello (0.00s)
    --- PASS: TestParsePath/jwt-1914071788362821795 (0.00s)
    --- PASS: TestParsePath/accounting-transit (0.00s)
=== RUN   TestPathParameters
=== RUN   TestPathParameters//transform/role/{name}
=== RUN   TestPathParameters//transit/sign/{name}/{urlalgorithm}
=== RUN   TestPathParameters//transit/sign/{name}/{urlalgorithm}#01
=== RUN   TestPathParameters//auth/approle/tidy/secret-id
=== RUN   TestPathParameters//sys/mfa/method/totp/{name}/admin-generate
--- PASS: TestPathParameters (0.00s)
    --- PASS: TestPathParameters//transform/role/{name} (0.00s)
    --- PASS: TestPathParameters//transit/sign/{name}/{urlalgorithm} (0.00s)
    --- PASS: TestPathParameters//transit/sign/{name}/{urlalgorithm}#01 (0.00s)
    --- PASS: TestPathParameters//auth/approle/tidy/secret-id (0.00s)
    --- PASS: TestPathParameters//sys/mfa/method/totp/{name}/admin-generate (0.00s)
PASS
ok  	github.com/terraform-providers/terraform-provider-vault/util	0.008s

</details>

+4023 -16

0 comment

40 changed files

tyrannosaurus-becks

pr closed time in 5 hours

push eventterraform-providers/terraform-provider-vault

catsby

commit sha 4ae10b17a13f0d9079b3c4df62511aa1d85d7304

update template docs and add template to sidebar

view details

push time in 4 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha 7643c881961f0246db781b3b7f6e80c00a14ab8f

update docs to fix formatting

view details

push time in 4 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha 123ca87e396aae2acd538edd88c9622fedf7fddf

doc format update

view details

push time in 4 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha ddfbb1e1aacc4bf50d149fca5909c4531471c56a

update link src

view details

push time in 4 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha 3160fcc917cb45a8bd94630d73fbaa5a4f149595

update links

view details

push time in 4 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha 66a3ccb16bdb35c88f57673aec13dce1e5b78754

update docs to remove _name and -name suffix

view details

push time in 4 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha b71d3ef362e9e3b8e8410c03eb7bb64270670821

update documentation to remove _name, jwt config generated docs

view details

push time in 5 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha 0c440e68ecd46086527aa695703565730756a115

update log formatting for %q

view details

push time in 5 days

issue commentterraform-providers/terraform-provider-vault

vault_jwt_auth_backend doesn't support importing

Hello there! This was patched in #806 and will go out in the next release (next few weeks). Thanks for your patience!

rosteroster

comment created time in 5 days

push eventterraform-providers/terraform-provider-vault

Clint

commit sha 59b8e2ffb8d01b08c017f6a83d94e86c1ed35945

Update CHANGELOG.md

view details

push time in 5 days

push eventterraform-providers/terraform-provider-vault

Clint

commit sha c3e2710b9825bb7114c6263cd159cc06dfe245f4

Jwt auth import (#806) * Add authMountTuneGet function to return tune information of a mount * refactor to use new authMountTuneGet function * Add Import for JWT auth backend. Also set's path, type, and tune options on READ * Update vault/auth_mount.go Co-authored-by: Theron Voran <tvoran@users.noreply.github.com> * Update vault/resource_jwt_auth_backend.go Co-authored-by: Theron Voran <tvoran@users.noreply.github.com> Co-authored-by: Theron Voran <tvoran@users.noreply.github.com>

view details

push time in 5 days

PR merged terraform-providers/terraform-provider-vault

Jwt auth import size/S

Fixes https://github.com/terraform-providers/terraform-provider-vault/issues/295 . This resource uses path for the ID, but because(?) it's a ForceNew attribute it was never read from the API. Attempting to import uses an ID input, but the path that was used for the READ operation was empty so no data was found. This will set the path to the Id() value if path is not found, so the READ should succeed.

In order to import without changes needed, this PR also sets tune and type information.

Community Note

  • Please vote on this pull request by adding a 👍 reaction to the original pull request comment to help the community and maintainers prioritize this request
  • Please do not leave "+1" comments, they generate extra noise for pull request followers and do not help prioritize the request

<!--- If your PR fully resolves and should automatically close the linked issue, use Closes. Otherwise, use Relates --->

Relates OR Closes #295

Release note for CHANGELOG:

- Update jwt_auth_backed to enable importing existing backends. 
- Update jwt_auth_backed to read `type` and `tune` information

Output from acceptance testing:

[terraform-provider-vault][jwt_auth_import](4)$ make testacc TESTARGS='-run=TestAccJWT'                                                                                         ✭
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go test $(go list ./... |grep -v 'vendor') -v -run=TestAccJWT -timeout 120m
?       github.com/terraform-providers/terraform-provider-vault [no test files]
?       github.com/terraform-providers/terraform-provider-vault/cmd/coverage    [no test files]
testing: warning: no tests to run
PASS
ok      github.com/terraform-providers/terraform-provider-vault/util    (cached) [no tests to run]
=== RUN   TestAccJWTAuthBackendRole_import
--- PASS: TestAccJWTAuthBackendRole_import (0.14s)
=== RUN   TestAccJWTAuthBackendRole_basic
--- PASS: TestAccJWTAuthBackendRole_basic (0.11s)
=== RUN   TestAccJWTAuthBackendRole_update
--- PASS: TestAccJWTAuthBackendRole_update (0.18s)
=== RUN   TestAccJWTAuthBackendRole_full
--- PASS: TestAccJWTAuthBackendRole_full (0.11s)
=== RUN   TestAccJWTAuthBackendRoleOIDC_full
--- PASS: TestAccJWTAuthBackendRoleOIDC_full (0.83s)
=== RUN   TestAccJWTAuthBackendRole_fullUpdate
--- PASS: TestAccJWTAuthBackendRole_fullUpdate (0.19s)
=== RUN   TestAccJWTAuthBackendRole_fullDeprecated
--- PASS: TestAccJWTAuthBackendRole_fullDeprecated (0.19s)
=== RUN   TestAccJWTAuthBackend
--- PASS: TestAccJWTAuthBackend (0.96s)
=== RUN   TestAccJWTAuthBackend_OIDC
--- PASS: TestAccJWTAuthBackend_OIDC (0.22s)
=== RUN   TestAccJWTAuthBackend_negative
--- PASS: TestAccJWTAuthBackend_negative (0.03s)
=== RUN   TestAccJWTAuthBackend_missingMandatory
--- PASS: TestAccJWTAuthBackend_missingMandatory (0.55s)
PASS
ok      github.com/terraform-providers/terraform-provider-vault/vault   3.773s


...
+36 -4

0 comment

3 changed files

catsby

pr closed time in 5 days

issue closedterraform-providers/terraform-provider-vault

vault_jwt_auth_backend doesn't support importing

Hi there,

Thank you for opening an issue. Please note that we try to keep the Terraform issue tracker reserved for bug reports and feature requests. For general usage questions, please see: https://www.terraform.io/community.html.

Terraform Version

v0.11.11

Affected Resource(s)

Please list the resources as a list, for example:

  • vault_jwt_auth_backend

If this issue appears to affect multiple resources, it may be an issue with Terraform's core, so please mention this.

Terraform Configuration Files

resource "vault_jwt_auth_backend" "sl_iam_jwt_auth" {
  path               = "jwt"
  oidc_discovery_url = "https://iam.strln.net"
  bound_issuer       = "https://iam.strln.net"
}

Debug Output

Panic Output

Expected Behavior

We should be able to do a terraform import for vault_jwt_auth_backend resources because otherwise we end up in a state where we can neither run "terraform apply" (it attempts and fails to create the backend again at the same path) nor can we import it to the terraform state easily, so an existing jwt backend puts us into a deadlock.

Actual Behavior

terraform import fails saying that it's not supported by this resource type

Attempted to fix by adding

Importer: &schema.ResourceImporter{
    State: schema.ImportStatePassthrough,
},

to the vault/resource_jwt_auth_backend.go schema.Resource but this started failing with the following error on import attempts:

vault_jwt_auth_backend.sl_iam_jwt_auth: Importing from ID "jwt"...
vault_jwt_auth_backend.sl_iam_jwt_auth: Import complete!
  Imported vault_jwt_auth_backend (ID: jwt)
vault_jwt_auth_backend.sl_iam_jwt_auth: Refreshing state... (ID: jwt)

Error: vault_jwt_auth_backend.sl_iam_jwt_auth (import id: jwt): 1 error(s) occurred:

* import vault_jwt_auth_backend.sl_iam_jwt_auth result: jwt: import vault_jwt_auth_backend.sl_iam_jwt_auth (id: jwt): Terraform detected a resource with this ID doesn't
exist. Please verify the ID is correct. You cannot import non-existent
resources using Terraform import.

We've confirmed via "vault auth list" & "vault auth read /auth/jwt/config" that this backend does in fact exist but the import is failing

Steps to Reproduce

Please list the steps required to reproduce the issue, for example:

  1. terraform apply

Important Factoids

The idea to that Importer: blah code segment came from the (functional) generic auth backend resource: https://github.com/terraform-providers/terraform-provider-vault/blob/master/vault/resource_auth_backend.go#L20-L22

References

Are there any other GitHub issues (open or closed) or Pull Requests that should be linked here? For example:

  • GH-1234

closed time in 5 days

rosteroster

push eventterraform-providers/terraform-provider-vault

Clint

commit sha 51cb4e57244067e558fe4d7e410b0077294c55cf

remove the jwt/config resource from the code generated files (#807) * remove the jwt/config resource from the code generated files * remove jwt/config from the registry

view details

push time in 5 days

PR merged terraform-providers/terraform-provider-vault

remove the jwt/config resource from the code generated files size/XL

Remove the new resource_jwt_config resource from feature-generated-transform branch. cc @pcman312 .

+0 -395

0 comment

4 changed files

catsby

pr closed time in 5 days

push eventterraform-providers/terraform-provider-vault

Clint

commit sha d4368bf7974baba9a4475dead078083413bafe6e

Update vault/resource_jwt_auth_backend.go Co-authored-by: Theron Voran <tvoran@users.noreply.github.com>

view details

push time in 5 days

push eventterraform-providers/terraform-provider-vault

Clint

commit sha 631eae9317ca5cd823df7423e1345af8fbc1e7ba

Update vault/auth_mount.go Co-authored-by: Theron Voran <tvoran@users.noreply.github.com>

view details

push time in 5 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha d90ed66cf5cbeb7aed198ed200f733a844fdb3ea

remove the jwt/config resource from the code generated files

view details

catsby

commit sha e89cd16e5879967714c27802b458670655891bbd

remove jwt/config from the registry

view details

catsby

commit sha 91ec0cbf9a136a6e8103c93a8b745c51c0295a38

Merge branch 'feature-generated-remove-jwt-config' into generate-updates * feature-generated-remove-jwt-config: remove jwt/config from the registry remove the jwt/config resource from the code generated files

view details

push time in 6 days

push eventterraform-providers/terraform-provider-vault

Michael Golowka

commit sha aecb7911b4c2ad2fb78abf98450c514d2a39df33

Remove _name suffix

view details

catsby

commit sha ec69a7fb2b000b23d6d5e666c13c55d18be9d639

Merge branch 'feature-generated-transform-engine' into generate-updates * feature-generated-transform-engine: Remove _name suffix

view details

push time in 6 days

push eventterraform-providers/terraform-provider-vault

catsby

commit sha e89cd16e5879967714c27802b458670655891bbd

remove jwt/config from the registry

view details

push time in 6 days

PR opened terraform-providers/terraform-provider-vault

remove the jwt/config resource from the code generated files

Remove the new resource_jwt_config resource from feature-generated-transform branch. cc @pcman312 . Draft because the tests are failing atm...

+0 -393

0 comment

3 changed files

pr created time in 6 days

PR opened terraform-providers/terraform-provider-vault

Jwt auth import

Fixes https://github.com/terraform-providers/terraform-provider-vault/issues/295

Community Note

  • Please vote on this pull request by adding a 👍 reaction to the original pull request comment to help the community and maintainers prioritize this request
  • Please do not leave "+1" comments, they generate extra noise for pull request followers and do not help prioritize the request

<!--- If your PR fully resolves and should automatically close the linked issue, use Closes. Otherwise, use Relates --->

Relates OR Closes #295

Release note for CHANGELOG:

- Update jwt_auth_backed to enable importing existing backends. 
- Update jwt_auth_backed to read `type` and `tune` information

Output from acceptance testing:

[terraform-provider-vault][jwt_auth_import](4)$ make testacc TESTARGS='-run=TestAccJWT'                                                                                         ✭
==> Checking that code complies with gofmt requirements...
TF_ACC=1 go test $(go list ./... |grep -v 'vendor') -v -run=TestAccJWT -timeout 120m
?       github.com/terraform-providers/terraform-provider-vault [no test files]
?       github.com/terraform-providers/terraform-provider-vault/cmd/coverage    [no test files]
testing: warning: no tests to run
PASS
ok      github.com/terraform-providers/terraform-provider-vault/util    (cached) [no tests to run]
=== RUN   TestAccJWTAuthBackendRole_import
--- PASS: TestAccJWTAuthBackendRole_import (0.14s)
=== RUN   TestAccJWTAuthBackendRole_basic
--- PASS: TestAccJWTAuthBackendRole_basic (0.11s)
=== RUN   TestAccJWTAuthBackendRole_update
--- PASS: TestAccJWTAuthBackendRole_update (0.18s)
=== RUN   TestAccJWTAuthBackendRole_full
--- PASS: TestAccJWTAuthBackendRole_full (0.11s)
=== RUN   TestAccJWTAuthBackendRoleOIDC_full
--- PASS: TestAccJWTAuthBackendRoleOIDC_full (0.83s)
=== RUN   TestAccJWTAuthBackendRole_fullUpdate
--- PASS: TestAccJWTAuthBackendRole_fullUpdate (0.19s)
=== RUN   TestAccJWTAuthBackendRole_fullDeprecated
--- PASS: TestAccJWTAuthBackendRole_fullDeprecated (0.19s)
=== RUN   TestAccJWTAuthBackend
--- PASS: TestAccJWTAuthBackend (0.96s)
=== RUN   TestAccJWTAuthBackend_OIDC
--- PASS: TestAccJWTAuthBackend_OIDC (0.22s)
=== RUN   TestAccJWTAuthBackend_negative
--- PASS: TestAccJWTAuthBackend_negative (0.03s)
=== RUN   TestAccJWTAuthBackend_missingMandatory
--- PASS: TestAccJWTAuthBackend_missingMandatory (0.55s)
PASS
ok      github.com/terraform-providers/terraform-provider-vault/vault   3.773s


...
+36 -4

0 comment

3 changed files

pr created time in 7 days

create barnchterraform-providers/terraform-provider-vault

branch : jwt_auth_import

created branch time in 7 days

push eventhashicorp/vault

Clint

commit sha b3f0e9badfaac44ef4300acf13144abb2cec25d3

Stepwise docker env (#9292) * add first stepwise test env, Docker, with example transit test * update transit stepwise test * add other tests that use stepwise * cleanup test, make names different than just 'transit' * return the stderr if compile fails with error * minor cleanups * minor cleanups * go mod vendor * cleanups * remove some extra code, and un-export some fields/methods * update vendor * remove reference to vault.CoreConfig, which really wasn't used anyway * update with go mod vendor * restore Precheck method to test cases * clean up some networking things; create networks with UUID, clean up during teardown * vendor stepwise * Update sdk/testing/stepwise/environments/docker/environment.go haha thanks :D Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update sdk/testing/stepwise/environments/docker/environment.go Great catch, thanks Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * fix redundant name * update error message in test * Update builtin/credential/userpass/stepwise_test.go More explicit error checking and responding Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update builtin/logical/aws/stepwise_test.go `test` -> `testFunc` Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update builtin/logical/transit/stepwise_test.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * fix typos * update error messages to provide clarity * Update sdk/testing/stepwise/environments/docker/environment.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * update error handling / collection in Teardown * panic if GenerateUUID returns an error * Update sdk/testing/stepwise/environments/docker/environment.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> * Update builtin/credential/userpass/stepwise_test.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com> * Update builtin/logical/aws/stepwise_test.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com> * Update builtin/logical/transit/stepwise_test.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com> * Update sdk/testing/stepwise/environments/docker/environment.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com> * import ordering * standardize on dc from rc for cluster * lowercase name * CreateAPIClient -> NewAPIClient * testWait -> ensure * go mod cleanup * cleanups * move fields and method around * make start and dockerclusternode private; use better random serial number * use better random for SerialNumber * add a timeout to the context used for terminating the docker container * Use a constant for the Docker client version * rearrange import statements Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com> Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com>

view details

push time in 10 days

PR merged hashicorp/vault

Stepwise docker env

This PR introduces the first environment for the Stepwise plugin testing framework, as well as 3 plugin tests that utilizes it:

With a local instance of Docker accessible, the tests can be ran using with the make testacc step:

$ make testacc TEST=./builtin/logical/transit TESTARGS="-count=1 -run=TestAcc"  
$ make testacc TEST=./builtin/credential/userpass TESTARGS="-count=1 -run=TestAcc"  
$ make testacc TEST=./builtin/logical/aws TESTARGS="-count=1 -run=TestAcc"  # requires TEST_AWS env vars

This is a work in progress which will continue beyond this PR, including tests for this environment which will follow later.

+42314 -30

1 comment

379 changed files

catsby

pr closed time in 10 days

pull request commenthashicorp/vault

Stepwise docker env

test-go-race is failing on the main branch, I have the go-ahead to merge as-is

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"+

Rearraged in 5467f2561

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 5467f25617d9bdda40c89d2dc10a67267dc712e2

rearrange import statements

view details

push time in 10 days

push eventhashicorp/vault

catsby

commit sha e9b48a7fdeaad35049419cf9148f1c5836b60788

Use a constant for the Docker client version

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package transit++import (+	"encoding/base64"+	"fmt"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/keysutil"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"github.com/mitchellh/mapstructure"++	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++// TestBackend_basic_docker is an example test using the Docker Environment+func TestAccBackend_basic_docker(t *testing.T) {+	decryptData := make(map[string]interface{})+	envOptions := stepwise.MountOptions{+		RegistryName:    "updatedtransit",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "transit",+		MountPathPrefix: "transit_temp",+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment("updatedtransit", &envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseListPolicy(t, "test", true),+			testAccStepwiseWritePolicy(t, "test", true),+			testAccStepwiseListPolicy(t, "test", false),+			testAccStepwiseReadPolicy(t, "test", false, true),+			testAccStepwiseEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData),+			testAccStepwiseDecrypt(t, "test", testPlaintext, decryptData),+			testAccStepwiseEnableDeletion(t, "test"),+			testAccStepwiseDeletePolicy(t, "test"),+			testAccStepwiseReadPolicy(t, "test", true, true),+		},+	})+}++func testAccStepwiseWritePolicy(t *testing.T, name string, derived bool) stepwise.Step {+	ts := stepwise.Step{+		Operation: stepwise.WriteOperation,+		Path:      "keys/" + name,+		Data: map[string]interface{}{+			"derived": derived,+		},+	}+	if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+		ts.Data["type"] = "chacha20-poly1305"+	}+	return ts+}++func testAccStepwiseListPolicy(t *testing.T, name string, expectNone bool) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ListOperation,+		Path:      "keys",+		Assert: func(resp *api.Secret, err error) error {+			if (resp == nil || len(resp.Data) == 0) && !expectNone {+				return fmt.Errorf("missing response")+			}+			if expectNone && resp != nil {+				return fmt.Errorf("response data when expecting none")+			}++			if expectNone && resp == nil {+				return nil+			}++			var d struct {+				Keys []string `mapstructure:"keys"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			if len(d.Keys) > 0 && d.Keys[0] != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if len(d.Keys) != 1 {+				return fmt.Errorf("only 1 key expected, %d returned", len(d.Keys))+			}+			return nil+		},+	}+}++func testAccStepwiseReadPolicy(t *testing.T, name string, expectNone, derived bool) stepwise.Step {+	t.Helper()+	return testAccStepwiseReadPolicyWithVersions(t, name, expectNone, derived, 1, 0)+}++func testAccStepwiseReadPolicyWithVersions(t *testing.T, name string, expectNone, derived bool, minDecryptionVersion int, minEncryptionVersion int) stepwise.Step {+	t.Helper()+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      "keys/" + name,+		Assert: func(resp *api.Secret, err error) error {+			t.Helper()+			if resp == nil && !expectNone {+				return fmt.Errorf("missing response")+			} else if expectNone {+				if resp != nil {+					return fmt.Errorf("response when expecting none")+				}+				return nil+			}+			var d struct {+				Name                 string           `mapstructure:"name"`+				Key                  []byte           `mapstructure:"key"`+				Keys                 map[string]int64 `mapstructure:"keys"`+				Type                 string           `mapstructure:"type"`+				Derived              bool             `mapstructure:"derived"`+				KDF                  string           `mapstructure:"kdf"`+				DeletionAllowed      bool             `mapstructure:"deletion_allowed"`+				ConvergentEncryption bool             `mapstructure:"convergent_encryption"`+				MinDecryptionVersion int              `mapstructure:"min_decryption_version"`+				MinEncryptionVersion int              `mapstructure:"min_encryption_version"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}++			if d.Name != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+				if d.Type != keysutil.KeyType(keysutil.KeyType_ChaCha20_Poly1305).String() {+					return fmt.Errorf("bad key type: %#v", d)+				}+			} else if d.Type != keysutil.KeyType(keysutil.KeyType_AES256_GCM96).String() {+				return fmt.Errorf("bad key type: %#v", d)+			}+			// Should NOT get a key back+			if d.Key != nil {+				return fmt.Errorf("bad: %#v", d)+			}+			if d.Keys == nil {+				return fmt.Errorf("bad: %#v", d)+			}+			if d.MinDecryptionVersion != minDecryptionVersion {+				return fmt.Errorf("bad: %#v", d)+			}+			if d.MinEncryptionVersion != minEncryptionVersion {+				return fmt.Errorf("bad: %#v", d)+			}+			if d.DeletionAllowed == true {+				return fmt.Errorf("bad: %#v", d)+			}+			if d.Derived != derived {+				return fmt.Errorf("bad: %#v", d)+			}+			if derived && d.KDF != "hkdf_sha256" {+				return fmt.Errorf("bad: %#v", d)+			}+			return nil+		},+	}+}++func testAccStepwiseEncryptContext(+	t *testing.T, name, plaintext, context string, decryptData map[string]interface{}) stepwise.Step {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {+	transport := cleanhttp.DefaultPooledTransport()+	transport.TLSClientConfig = n.TLSConfig.Clone()+	if err := http2.ConfigureTransport(transport); err != nil {+		return nil, err+	}+	client := &http.Client{+		Transport: transport,+		CheckRedirect: func(*http.Request, []*http.Request) error {+			// This can of course be overridden per-test by using its own client+			return fmt.Errorf("redirects not allowed in these tests")+		},+	}+	config := api.DefaultConfig()+	if config.Error != nil {+		return nil, config.Error+	}+	config.Address = fmt.Sprintf("https://127.0.0.1:%s", n.HostPort)+	config.HttpClient = client+	config.MaxRetries = 0+	apiClient, err := api.NewClient(config)+	if err != nil {+		return nil, err+	}+	apiClient.SetToken(n.Cluster.RootToken())+	return apiClient, nil+}++// Cleanup kills the container of the node+func (n *DockerClusterNode) Cleanup() error {+	return n.dockerAPI.ContainerKill(context.Background(), n.container.ID, "KILL")+}++func (n *DockerClusterNode) Start(cli *docker.Client, caDir, netName string, netCIDR *DockerClusterNode, pluginBinPath string) error {+	n.dockerAPI = cli++	err := n.setupCert()+	if err != nil {+		return err+	}++	vaultCfg := map[string]interface{}{+		"listener": map[string]interface{}{+			"tcp": map[string]interface{}{+				"address":       fmt.Sprintf("%s:%d", "0.0.0.0", 8200),+				"tls_cert_file": "/vault/config/cert.pem",+				"tls_key_file":  "/vault/config/key.pem",+				"telemetry": map[string]interface{}{+					"unauthenticated_metrics_access": true,+				},+			},+		},+		"telemetry": map[string]interface{}{+			"disable_hostname": true,+		},+		"storage": map[string]interface{}{+			"raft": map[string]interface{}{+				"path":    "/vault/file",+				"node_id": n.NodeID,+			},+		},+		"cluster_name":         netName,+		"log_level":            "TRACE",+		"raw_storage_endpoint": true,+		"plugin_directory":     "/vault/config",+		// disable_mlock is required for working in the Docker environment with+		// custom plugins+		"disable_mlock": true,+		// These are being provided by docker-entrypoint now, since we don't know+		// the address before the container starts.+		//"api_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port),+		//"cluster_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port+1),+	}+	cfgJSON, err := json.Marshal(vaultCfg)+	if err != nil {+		return err+	}++	err = ioutil.WriteFile(filepath.Join(n.WorkDir, "local.json"), cfgJSON, 0644)+	if err != nil {+		return err+	}

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"fmt"+	"io/ioutil"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/docker/api/types/strslice"+	docker "github.com/docker/docker/client"+	"github.com/docker/docker/pkg/archive"+)++// Runner manages the lifecycle of the Docker container+type Runner struct {+	dockerAPI       *docker.Client+	ContainerConfig *container.Config+	ContainerName   string+	NetName         string+	IP              string+	CopyFromTo      map[string]string

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package transit++import (+	"encoding/base64"+	"fmt"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/keysutil"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"github.com/mitchellh/mapstructure"++	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++// TestBackend_basic_docker is an example test using the Docker Environment+func TestAccBackend_basic_docker(t *testing.T) {+	decryptData := make(map[string]interface{})+	envOptions := stepwise.MountOptions{+		RegistryName:    "updatedtransit",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "transit",+		MountPathPrefix: "transit_temp",+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment("updatedtransit", &envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseListPolicy(t, "test", true),+			testAccStepwiseWritePolicy(t, "test", true),+			testAccStepwiseListPolicy(t, "test", false),+			testAccStepwiseReadPolicy(t, "test", false, true),+			testAccStepwiseEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData),+			testAccStepwiseDecrypt(t, "test", testPlaintext, decryptData),+			testAccStepwiseEnableDeletion(t, "test"),+			testAccStepwiseDeletePolicy(t, "test"),+			testAccStepwiseReadPolicy(t, "test", true, true),+		},+	})+}++func testAccStepwiseWritePolicy(t *testing.T, name string, derived bool) stepwise.Step {+	ts := stepwise.Step{+		Operation: stepwise.WriteOperation,+		Path:      "keys/" + name,+		Data: map[string]interface{}{+			"derived": derived,+		},+	}+	if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+		ts.Data["type"] = "chacha20-poly1305"+	}+	return ts+}++func testAccStepwiseListPolicy(t *testing.T, name string, expectNone bool) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ListOperation,+		Path:      "keys",+		Assert: func(resp *api.Secret, err error) error {+			if (resp == nil || len(resp.Data) == 0) && !expectNone {+				return fmt.Errorf("missing response")+			}+			if expectNone && resp != nil {+				return fmt.Errorf("response data when expecting none")+			}++			if expectNone && resp == nil {+				return nil+			}++			var d struct {+				Keys []string `mapstructure:"keys"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			if len(d.Keys) > 0 && d.Keys[0] != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if len(d.Keys) != 1 {+				return fmt.Errorf("only 1 key expected, %d returned", len(d.Keys))+			}+			return nil+		},+	}+}++func testAccStepwiseReadPolicy(t *testing.T, name string, expectNone, derived bool) stepwise.Step {+	t.Helper()+	return testAccStepwiseReadPolicyWithVersions(t, name, expectNone, derived, 1, 0)+}++func testAccStepwiseReadPolicyWithVersions(t *testing.T, name string, expectNone, derived bool, minDecryptionVersion int, minEncryptionVersion int) stepwise.Step {+	t.Helper()+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      "keys/" + name,+		Assert: func(resp *api.Secret, err error) error {+			t.Helper()+			if resp == nil && !expectNone {+				return fmt.Errorf("missing response")+			} else if expectNone {+				if resp != nil {+					return fmt.Errorf("response when expecting none")+				}+				return nil+			}+			var d struct {+				Name                 string           `mapstructure:"name"`+				Key                  []byte           `mapstructure:"key"`+				Keys                 map[string]int64 `mapstructure:"keys"`+				Type                 string           `mapstructure:"type"`+				Derived              bool             `mapstructure:"derived"`+				KDF                  string           `mapstructure:"kdf"`+				DeletionAllowed      bool             `mapstructure:"deletion_allowed"`+				ConvergentEncryption bool             `mapstructure:"convergent_encryption"`+				MinDecryptionVersion int              `mapstructure:"min_decryption_version"`+				MinEncryptionVersion int              `mapstructure:"min_encryption_version"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}++			if d.Name != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+				if d.Type != keysutil.KeyType(keysutil.KeyType_ChaCha20_Poly1305).String() {+					return fmt.Errorf("bad key type: %#v", d)+				}+			} else if d.Type != keysutil.KeyType(keysutil.KeyType_AES256_GCM96).String() {+				return fmt.Errorf("bad key type: %#v", d)+			}+			// Should NOT get a key back+			if d.Key != nil {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package transit++import (+	"encoding/base64"+	"fmt"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/keysutil"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"github.com/mitchellh/mapstructure"++	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++// TestBackend_basic_docker is an example test using the Docker Environment+func TestAccBackend_basic_docker(t *testing.T) {+	decryptData := make(map[string]interface{})+	envOptions := stepwise.MountOptions{+		RegistryName:    "updatedtransit",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "transit",+		MountPathPrefix: "transit_temp",+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment("updatedtransit", &envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseListPolicy(t, "test", true),+			testAccStepwiseWritePolicy(t, "test", true),+			testAccStepwiseListPolicy(t, "test", false),+			testAccStepwiseReadPolicy(t, "test", false, true),+			testAccStepwiseEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData),+			testAccStepwiseDecrypt(t, "test", testPlaintext, decryptData),+			testAccStepwiseEnableDeletion(t, "test"),+			testAccStepwiseDeletePolicy(t, "test"),+			testAccStepwiseReadPolicy(t, "test", true, true),+		},+	})+}++func testAccStepwiseWritePolicy(t *testing.T, name string, derived bool) stepwise.Step {+	ts := stepwise.Step{+		Operation: stepwise.WriteOperation,+		Path:      "keys/" + name,+		Data: map[string]interface{}{+			"derived": derived,+		},+	}+	if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+		ts.Data["type"] = "chacha20-poly1305"+	}+	return ts+}++func testAccStepwiseListPolicy(t *testing.T, name string, expectNone bool) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ListOperation,+		Path:      "keys",+		Assert: func(resp *api.Secret, err error) error {+			if (resp == nil || len(resp.Data) == 0) && !expectNone {+				return fmt.Errorf("missing response")+			}+			if expectNone && resp != nil {+				return fmt.Errorf("response data when expecting none")+			}++			if expectNone && resp == nil {+				return nil+			}++			var d struct {+				Keys []string `mapstructure:"keys"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			if len(d.Keys) > 0 && d.Keys[0] != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if len(d.Keys) != 1 {+				return fmt.Errorf("only 1 key expected, %d returned", len(d.Keys))+			}+			return nil+		},+	}+}++func testAccStepwiseReadPolicy(t *testing.T, name string, expectNone, derived bool) stepwise.Step {+	t.Helper()

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package aws++import (+	"log"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_Stepwise_basic(t *testing.T) {+	t.Parallel()

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(+	t *testing.T, name string, password string, policies string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "users/" + name,+		Data: map[string]interface{}{+			"password": password,+			"policies": policies,+		},+	}+}++func testAccStepwiseDeleteUser(t *testing.T, n string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.DeleteOperation,+		Path:      "users/" + n,+	}+}++func testAccStepwiseReadUser(t *testing.T, name string, policies string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      "users/" + name,+		Assert: func(resp *api.Secret, err error) error {+			if resp == nil {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(+	t *testing.T, name string, password string, policies string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "users/" + name,+		Data: map[string]interface{}{+			"password": password,+			"policies": policies,+		},+	}+}++func testAccStepwiseDeleteUser(t *testing.T, n string) stepwise.Step {

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(

Deferring and tracking this internally, to follow up in the coming weeks

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {+	transport := cleanhttp.DefaultPooledTransport()+	transport.TLSClientConfig = n.TLSConfig.Clone()+	if err := http2.ConfigureTransport(transport); err != nil {+		return nil, err+	}+	client := &http.Client{+		Transport: transport,+		CheckRedirect: func(*http.Request, []*http.Request) error {+			// This can of course be overridden per-test by using its own client+			return fmt.Errorf("redirects not allowed in these tests")+		},+	}+	config := api.DefaultConfig()+	if config.Error != nil {+		return nil, config.Error+	}+	config.Address = fmt.Sprintf("https://127.0.0.1:%s", n.HostPort)+	config.HttpClient = client+	config.MaxRetries = 0+	apiClient, err := api.NewClient(config)+	if err != nil {+		return nil, err+	}+	apiClient.SetToken(n.Cluster.RootToken())+	return apiClient, nil+}++// Cleanup kills the container of the node+func (n *DockerClusterNode) Cleanup() error {+	return n.dockerAPI.ContainerKill(context.Background(), n.container.ID, "KILL")

Added a context.WithTimeout in b95339611

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha b9533961185b06fefe9845d7992dd6716022d82b

add a timeout to the context used for terminating the docker container

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),

Whoops, I forgot that one. I gave it the same treatment. Ideally I can follow-up and use other built-in libraries for the certificate parts and not need any of this code long term

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha e38291773a26a0661983c203eee08ce32bfbac52

use better random for SerialNumber

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {+	transport := cleanhttp.DefaultPooledTransport()+	transport.TLSClientConfig = n.TLSConfig.Clone()+	if err := http2.ConfigureTransport(transport); err != nil {+		return nil, err+	}+	client := &http.Client{+		Transport: transport,+		CheckRedirect: func(*http.Request, []*http.Request) error {+			// This can of course be overridden per-test by using its own client+			return fmt.Errorf("redirects not allowed in these tests")+		},+	}+	config := api.DefaultConfig()+	if config.Error != nil {+		return nil, config.Error+	}+	config.Address = fmt.Sprintf("https://127.0.0.1:%s", n.HostPort)+	config.HttpClient = client+	config.MaxRetries = 0+	apiClient, err := api.NewClient(config)+	if err != nil {+		return nil, err+	}+	apiClient.SetToken(n.Cluster.RootToken())+	return apiClient, nil+}++// Cleanup kills the container of the node+func (n *DockerClusterNode) Cleanup() error {+	return n.dockerAPI.ContainerKill(context.Background(), n.container.ID, "KILL")+}++func (n *DockerClusterNode) Start(cli *docker.Client, caDir, netName string, netCIDR *DockerClusterNode, pluginBinPath string) error {

I made the Start and DockerClusterNode private in 863f2a1ba

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),

Changed in 863f2a1ba , please take a look

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 863f2a1ba9d8ba94f08ac79482ff18b7afb70a91

make start and dockerclusternode private; use better random serial number

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {+	transport := cleanhttp.DefaultPooledTransport()+	transport.TLSClientConfig = n.TLSConfig.Clone()+	if err := http2.ConfigureTransport(transport); err != nil {+		return nil, err+	}+	client := &http.Client{+		Transport: transport,+		CheckRedirect: func(*http.Request, []*http.Request) error {+			// This can of course be overridden per-test by using its own client+			return fmt.Errorf("redirects not allowed in these tests")+		},+	}+	config := api.DefaultConfig()+	if config.Error != nil {+		return nil, config.Error+	}+	config.Address = fmt.Sprintf("https://127.0.0.1:%s", n.HostPort)+	config.HttpClient = client+	config.MaxRetries = 0+	apiClient, err := api.NewClient(config)+	if err != nil {+		return nil, err+	}+	apiClient.SetToken(n.Cluster.RootToken())+	return apiClient, nil+}++// Cleanup kills the container of the node+func (n *DockerClusterNode) Cleanup() error {+	return n.dockerAPI.ContainerKill(context.Background(), n.container.ID, "KILL")+}++func (n *DockerClusterNode) Start(cli *docker.Client, caDir, netName string, netCIDR *DockerClusterNode, pluginBinPath string) error {+	n.dockerAPI = cli++	err := n.setupCert()+	if err != nil {+		return err+	}++	vaultCfg := map[string]interface{}{+		"listener": map[string]interface{}{+			"tcp": map[string]interface{}{+				"address":       fmt.Sprintf("%s:%d", "0.0.0.0", 8200),+				"tls_cert_file": "/vault/config/cert.pem",+				"tls_key_file":  "/vault/config/key.pem",+				"telemetry": map[string]interface{}{+					"unauthenticated_metrics_access": true,+				},+			},+		},+		"telemetry": map[string]interface{}{+			"disable_hostname": true,+		},+		"storage": map[string]interface{}{+			"raft": map[string]interface{}{+				"path":    "/vault/file",+				"node_id": n.NodeID,+			},+		},+		"cluster_name":         netName,+		"log_level":            "TRACE",+		"raw_storage_endpoint": true,+		"plugin_directory":     "/vault/config",+		// disable_mlock is required for working in the Docker environment with+		// custom plugins+		"disable_mlock": true,+		// These are being provided by docker-entrypoint now, since we don't know+		// the address before the container starts.+		//"api_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port),+		//"cluster_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port+1),+	}+	cfgJSON, err := json.Marshal(vaultCfg)+	if err != nil {+		return err+	}++	err = ioutil.WriteFile(filepath.Join(n.WorkDir, "local.json"), cfgJSON, 0644)+	if err != nil {+		return err+	}+	// setup plugin bin copy if needed+	copyFromTo := map[string]string{+		n.WorkDir: "/vault/config",+		caDir:     "/usr/local/share/ca-certificates/",+	}+	if pluginBinPath != "" {+		base := path.Base(pluginBinPath)+		copyFromTo[pluginBinPath] = filepath.Join("/vault/config", base)+	}++	r := &Runner{+		dockerAPI: cli,+		ContainerConfig: &container.Config{+			Image: "vault",+			Entrypoint: []string{"/bin/sh", "-c", "update-ca-certificates && " ++				"exec /usr/local/bin/docker-entrypoint.sh vault server -log-level=trace -dev-plugin-dir=./vault/config -config /vault/config/local.json"},+			Env: []string{+				"VAULT_CLUSTER_INTERFACE=eth0",+				"VAULT_API_ADDR=https://127.0.0.1:8200",+				fmt.Sprintf("VAULT_REDIRECT_ADDR=https://%s:8200", n.Name()),+			},+			Labels:       nil,+			ExposedPorts: nat.PortSet{"8200/tcp": {}, "8201/tcp": {}},+		},+		ContainerName: n.Name(),+		NetName:       netName,+		CopyFromTo:    copyFromTo,+	}++	n.container, err = r.Start(context.Background())+	if err != nil {+		return err+	}++	n.Address = &net.TCPAddr{+		IP:   net.ParseIP(n.container.NetworkSettings.IPAddress),+		Port: 8200,+	}+	ports := n.container.NetworkSettings.NetworkSettingsBase.Ports[nat.Port("8200/tcp")]+	if len(ports) == 0 {+		n.Cleanup()+		return fmt.Errorf("could not find port binding for 8200/tcp")+	}+	n.HostPort = ports[0].HostPort++	return nil+}++// DockerClusterOptions has options for setting up the docker cluster+type DockerClusterOptions struct {+	KeepStandbysSealed bool+	RequireClientAuth  bool+	SkipInit           bool+	CACert             []byte+	NumCores           int+	tmpDir             string+	PluginTestBin      string+	// SetupFunc is called after the cluster is started.+	SetupFunc func(t testing.T, c *DockerCluster)+	CAKey     *ecdsa.PrivateKey+}++//+// test methods/functions+//++// testWaitHealthMatches checks health+func testWaitHealthMatches(ctx context.Context, client *api.Client, ready func(response *api.HealthResponse) error) error {+	var health *api.HealthResponse+	var err error+	for ctx.Err() == nil {+		health, err = client.Sys().Health()+		switch {+		case err != nil:+		case health == nil:+			err = fmt.Errorf("nil response to health check")+		default:+			err = ready(health)+			if err == nil {+				return nil+			}+		}+		time.Sleep(500 * time.Millisecond)+	}+	return fmt.Errorf("error checking health: %v", err)+}++func testWaitLeaderMatches(ctx context.Context, client *api.Client, ready func(response *api.LeaderResponse) error) error {+	var leader *api.LeaderResponse+	var err error+	for ctx.Err() == nil {+		leader, err = client.Sys().Leader()+		switch {+		case err != nil:+		case leader == nil:+			err = fmt.Errorf("nil response to leader check")+		default:+			err = ready(leader)+			if err == nil {+				return nil+			}+		}+		time.Sleep(500 * time.Millisecond)+	}+	return fmt.Errorf("error checking leader: %v", err)+}++// end test helper methods++// TODO: allow number of cores/servers to be configurable+var DefaultNumCores = 1++// creates a managed docker container running Vault+func (cluster *DockerCluster) setupDockerCluster(opts *DockerClusterOptions) error {+	if opts != nil && opts.tmpDir != "" {+		if _, err := os.Stat(opts.tmpDir); os.IsNotExist(err) {+			if err := os.MkdirAll(opts.tmpDir, 0700); err != nil {+				return err+			}+		}+		cluster.tmpDir = opts.tmpDir+	} else {+		tempDir, err := ioutil.TempDir("", "vault-test-cluster-")+		if err != nil {+			return err+		}+		cluster.tmpDir = tempDir+	}+	caDir := filepath.Join(cluster.tmpDir, "ca")+	if err := os.MkdirAll(caDir, 0755); err != nil {+		return err+	}++	var numCores int+	if opts == nil || opts.NumCores == 0 {+		numCores = DefaultNumCores+	} else {+		numCores = opts.NumCores+	}++	if opts != nil && opts.RequireClientAuth {+		cluster.clientAuthRequired = true+	}++	for i := 0; i < numCores; i++ {+		nodeID := fmt.Sprintf("vault-%d", i)+		node := &DockerClusterNode{+			NodeID:  nodeID,+			Cluster: cluster,+			WorkDir: filepath.Join(cluster.tmpDir, nodeID),+		}+		cluster.ClusterNodes = append(cluster.ClusterNodes, node)+		if err := os.MkdirAll(node.WorkDir, 0700); err != nil {+			return err+		}+	}++	err := cluster.setupCA(opts)+	if err != nil {+		return err+	}++	cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+	if err != nil {+		return err+	}++	netUUID, _ := uuid.GenerateUUID()+	netName := fmt.Sprintf("%s-%s", "vault-test", netUUID)+	netID, err := setupNetwork(cli, netName)+	if err != nil {+		return err+	}+	cluster.networkID = netID++	for _, node := range cluster.ClusterNodes {+		pluginBinPath := ""+		if opts != nil {+			pluginBinPath = opts.PluginTestBin+		}++		err := node.Start(cli, caDir, netName, node, pluginBinPath)+		if err != nil {+			return err+		}+	}++	if opts == nil || !opts.SkipInit {+		if err := cluster.Initialize(context.Background()); err != nil {+			return err+		}+	}++	return nil+}++// Docker networking functions+// setupNetwork establishes networking for the Docker container+func setupNetwork(cli *docker.Client, netName string) (string, error) {+	id, err := createNetwork(cli, netName)+	if err != nil {+		return "", fmt.Errorf("couldn't create network %s: %w", netName, err)+	}+	return id, nil+}++func createNetwork(cli *docker.Client, netName string) (string, error) {+	resp, err := cli.NetworkCreate(context.Background(), netName, types.NetworkCreate{+		CheckDuplicate: true,+		Driver:         "bridge",+		Options:        map[string]string{},+		IPAM: &network.IPAM{+			Driver:  "default",+			Options: map[string]string{},+		},+	})+	if err != nil {+		return "", err+	}++	return resp.ID, nil+}++// NewEnvironment creats a new Stepwise Environment for executing tests+func NewEnvironment(name string, options *stepwise.MountOptions) *DockerCluster {

Moved in 813afbd5f

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey

Reordered in 813afbd5f

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 813afbd5f9e9ebf146bb52f2c1aa9e4bc33150fc

move fields and method around

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package aws++import (+	"log"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_Stepwise_basic(t *testing.T) {+	t.Parallel()+	envOptions := &stepwise.MountOptions{+		RegistryName:    "aws-sec",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "aws",+		MountPathPrefix: "aws-sec",+	}+	roleName := "vault-stepwise-role"+	stepwise.Run(t, stepwise.Case{+		Precheck:    func() { testAccStepwisePreCheck(t) },+		Environment: dockerEnvironment.NewEnvironment("aws", envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseConfig(t),+			testAccStepwiseWritePolicy(t, roleName, testDynamoPolicy),+			testAccStepwiseRead(t, "creds", roleName, []credentialTestFunc{listDynamoTablesTest}),+		},+	})+}++func testAccStepwiseConfig(t *testing.T) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "config/root",+		Data: map[string]interface{}{+			"region":     os.Getenv("AWS_DEFAULT_REGION"),+			"access_key": os.Getenv("TEST_AWS_ACCESS_KEY"),+			"secret_key": os.Getenv("TEST_AWS_SECRET_KEY"),+		},+	}+}++func testAccStepwiseWritePolicy(t *testing.T, name string, policy string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "roles/" + name,+		Data: map[string]interface{}{+			"policy_document": policy,+			"credential_type": "iam_user",+		},+	}+}++func testAccStepwiseRead(t *testing.T, path, name string, credentialTests []credentialTestFunc) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      path + "/" + name,+		Assert: func(resp *api.Secret, err error) error {+			if err != nil {+				return err+			}+			var d struct {+				AccessKey string `mapstructure:"access_key"`+				SecretKey string `mapstructure:"secret_key"`+				STSToken  string `mapstructure:"security_token"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			log.Printf("[WARN] Generated credentials: %v", d)+			for _, test := range credentialTests {+				err := test(d.AccessKey, d.SecretKey, d.STSToken)+				if err != nil {+					return err+				}+			}+			return nil+		},+	}+}++func testAccStepwisePreCheck(t *testing.T) {+	initSetup.Do(func() {+		if v := os.Getenv("AWS_DEFAULT_REGION"); v == "" {+			log.Println("[INFO] Test: Using us-west-2 as test region")

changed in efb823fd3

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(+	t *testing.T, name string, password string, policies string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "users/" + name,+		Data: map[string]interface{}{+			"password": password,+			"policies": policies,+		},+	}+}++func testAccStepwiseDeleteUser(t *testing.T, n string) stepwise.Step {

changed in efb823fd3

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package aws++import (+	"log"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_Stepwise_basic(t *testing.T) {+	t.Parallel()+	envOptions := &stepwise.MountOptions{+		RegistryName:    "aws-sec",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "aws",+		MountPathPrefix: "aws-sec",+	}+	roleName := "vault-stepwise-role"+	stepwise.Run(t, stepwise.Case{+		Precheck:    func() { testAccStepwisePreCheck(t) },+		Environment: dockerEnvironment.NewEnvironment("aws", envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseConfig(t),+			testAccStepwiseWritePolicy(t, roleName, testDynamoPolicy),+			testAccStepwiseRead(t, "creds", roleName, []credentialTestFunc{listDynamoTablesTest}),+		},+	})+}++func testAccStepwiseConfig(t *testing.T) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "config/root",+		Data: map[string]interface{}{+			"region":     os.Getenv("AWS_DEFAULT_REGION"),+			"access_key": os.Getenv("TEST_AWS_ACCESS_KEY"),+			"secret_key": os.Getenv("TEST_AWS_SECRET_KEY"),+		},+	}+}++func testAccStepwiseWritePolicy(t *testing.T, name string, policy string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "roles/" + name,+		Data: map[string]interface{}{+			"policy_document": policy,+			"credential_type": "iam_user",+		},+	}+}++func testAccStepwiseRead(t *testing.T, path, name string, credentialTests []credentialTestFunc) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      path + "/" + name,+		Assert: func(resp *api.Secret, err error) error {+			if err != nil {+				return err+			}+			var d struct {+				AccessKey string `mapstructure:"access_key"`+				SecretKey string `mapstructure:"secret_key"`+				STSToken  string `mapstructure:"security_token"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			log.Printf("[WARN] Generated credentials: %v", d)

changed in efb823fd3

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 0417e6564cf1bb7e9a104899544b48b8f8019669

go mod cleanup

view details

catsby

commit sha efb823fd32878cea81f8c6cfb8aa099cb35cf787

cleanups

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package aws++import (+	"log"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_Stepwise_basic(t *testing.T) {+	t.Parallel()+	envOptions := &stepwise.MountOptions{+		RegistryName:    "aws-sec",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "aws",+		MountPathPrefix: "aws-sec",+	}+	roleName := "vault-stepwise-role"+	stepwise.Run(t, stepwise.Case{+		Precheck:    func() { testAccStepwisePreCheck(t) },+		Environment: dockerEnvironment.NewEnvironment("aws", envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseConfig(t),+			testAccStepwiseWritePolicy(t, roleName, testDynamoPolicy),+			testAccStepwiseRead(t, "creds", roleName, []credentialTestFunc{listDynamoTablesTest}),+		},+	})+}++func testAccStepwiseConfig(t *testing.T) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "config/root",+		Data: map[string]interface{}{+			"region":     os.Getenv("AWS_DEFAULT_REGION"),+			"access_key": os.Getenv("TEST_AWS_ACCESS_KEY"),+			"secret_key": os.Getenv("TEST_AWS_SECRET_KEY"),+		},+	}+}++func testAccStepwiseWritePolicy(t *testing.T, name string, policy string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "roles/" + name,+		Data: map[string]interface{}{+			"policy_document": policy,+			"credential_type": "iam_user",+		},+	}+}++func testAccStepwiseRead(t *testing.T, path, name string, credentialTests []credentialTestFunc) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      path + "/" + name,+		Assert: func(resp *api.Secret, err error) error {+			if err != nil {+				return err+			}+			var d struct {+				AccessKey string `mapstructure:"access_key"`+				SecretKey string `mapstructure:"secret_key"`+				STSToken  string `mapstructure:"security_token"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			log.Printf("[WARN] Generated credentials: %v", d)

changed in efb823fd3

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {+	transport := cleanhttp.DefaultPooledTransport()+	transport.TLSClientConfig = n.TLSConfig.Clone()+	if err := http2.ConfigureTransport(transport); err != nil {+		return nil, err+	}+	client := &http.Client{+		Transport: transport,+		CheckRedirect: func(*http.Request, []*http.Request) error {+			// This can of course be overridden per-test by using its own client+			return fmt.Errorf("redirects not allowed in these tests")+		},+	}+	config := api.DefaultConfig()+	if config.Error != nil {+		return nil, config.Error+	}+	config.Address = fmt.Sprintf("https://127.0.0.1:%s", n.HostPort)+	config.HttpClient = client+	config.MaxRetries = 0+	apiClient, err := api.NewClient(config)+	if err != nil {+		return nil, err+	}+	apiClient.SetToken(n.Cluster.RootToken())+	return apiClient, nil+}++// Cleanup kills the container of the node+func (n *DockerClusterNode) Cleanup() error {+	return n.dockerAPI.ContainerKill(context.Background(), n.container.ID, "KILL")+}++func (n *DockerClusterNode) Start(cli *docker.Client, caDir, netName string, netCIDR *DockerClusterNode, pluginBinPath string) error {+	n.dockerAPI = cli++	err := n.setupCert()+	if err != nil {+		return err+	}++	vaultCfg := map[string]interface{}{+		"listener": map[string]interface{}{+			"tcp": map[string]interface{}{+				"address":       fmt.Sprintf("%s:%d", "0.0.0.0", 8200),+				"tls_cert_file": "/vault/config/cert.pem",+				"tls_key_file":  "/vault/config/key.pem",+				"telemetry": map[string]interface{}{+					"unauthenticated_metrics_access": true,+				},+			},+		},+		"telemetry": map[string]interface{}{+			"disable_hostname": true,+		},+		"storage": map[string]interface{}{+			"raft": map[string]interface{}{+				"path":    "/vault/file",+				"node_id": n.NodeID,+			},+		},+		"cluster_name":         netName,+		"log_level":            "TRACE",+		"raw_storage_endpoint": true,+		"plugin_directory":     "/vault/config",+		// disable_mlock is required for working in the Docker environment with+		// custom plugins+		"disable_mlock": true,+		// These are being provided by docker-entrypoint now, since we don't know+		// the address before the container starts.+		//"api_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port),+		//"cluster_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port+1),+	}+	cfgJSON, err := json.Marshal(vaultCfg)+	if err != nil {+		return err+	}++	err = ioutil.WriteFile(filepath.Join(n.WorkDir, "local.json"), cfgJSON, 0644)+	if err != nil {+		return err+	}+	// setup plugin bin copy if needed+	copyFromTo := map[string]string{+		n.WorkDir: "/vault/config",+		caDir:     "/usr/local/share/ca-certificates/",+	}+	if pluginBinPath != "" {+		base := path.Base(pluginBinPath)+		copyFromTo[pluginBinPath] = filepath.Join("/vault/config", base)+	}++	r := &Runner{+		dockerAPI: cli,+		ContainerConfig: &container.Config{+			Image: "vault",+			Entrypoint: []string{"/bin/sh", "-c", "update-ca-certificates && " ++				"exec /usr/local/bin/docker-entrypoint.sh vault server -log-level=trace -dev-plugin-dir=./vault/config -config /vault/config/local.json"},+			Env: []string{+				"VAULT_CLUSTER_INTERFACE=eth0",+				"VAULT_API_ADDR=https://127.0.0.1:8200",+				fmt.Sprintf("VAULT_REDIRECT_ADDR=https://%s:8200", n.Name()),+			},+			Labels:       nil,+			ExposedPorts: nat.PortSet{"8200/tcp": {}, "8201/tcp": {}},+		},+		ContainerName: n.Name(),+		NetName:       netName,+		CopyFromTo:    copyFromTo,+	}++	n.container, err = r.Start(context.Background())+	if err != nil {+		return err+	}++	n.Address = &net.TCPAddr{+		IP:   net.ParseIP(n.container.NetworkSettings.IPAddress),+		Port: 8200,+	}+	ports := n.container.NetworkSettings.NetworkSettingsBase.Ports[nat.Port("8200/tcp")]+	if len(ports) == 0 {+		n.Cleanup()+		return fmt.Errorf("could not find port binding for 8200/tcp")+	}+	n.HostPort = ports[0].HostPort++	return nil+}++// DockerClusterOptions has options for setting up the docker cluster+type DockerClusterOptions struct {+	KeepStandbysSealed bool+	RequireClientAuth  bool+	SkipInit           bool+	CACert             []byte+	NumCores           int+	tmpDir             string+	PluginTestBin      string+	// SetupFunc is called after the cluster is started.+	SetupFunc func(t testing.T, c *DockerCluster)+	CAKey     *ecdsa.PrivateKey+}++//+// test methods/functions+//++// testWaitHealthMatches checks health+func testWaitHealthMatches(ctx context.Context, client *api.Client, ready func(response *api.HealthResponse) error) error {

Agree, I updated the names from testWait* to ensure* in 61e1d39ff

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 61e1d39ff361086f735a38b465c5930f92948521

testWait -> ensure

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(ctx, 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {

Applied in 39bb77ba9

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 39bb77ba9189934a69d2c48c37d36bbca0a63cca

CreateAPIClient -> NewAPIClient

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"

I lower cased the name in 113085c02. It's use is not apparent now but future plans may use it.

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 113085c023d77f8adf94d2870d2db7b1bd98dbbd

lowercase name

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}

Calling Append with nil is a no-op, so this is fine to do. I read Append's documentation and it does not specify. I read the code and assumed but had to try it out in the go-playground to confirm (https://play.golang.org/p/j9epRT-kFUd).

That all said I'm going to leave this as-is so future readers don't wonder the same thing and have to read the code to decipher. I feel like the if err := method(); err != nil {} is idiomatic so it should be familiar.

catsby

comment created time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	ClusterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {

That's a left-over from the prototype code I borrowed this from, where the cluster type was RealCluster and not DockerCluster. I renamed them all dc in 4e548aff7

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 6c7d2a6fc0b310e2193d270c26bdd34a0dca1f4a

import ordering

view details

catsby

commit sha 4e548aff716a82fee39a44c5070633906afea5a7

standardize on dc from rc for cluster

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha 33a8840cd1445c979d7746a148d7d4dcfc225570

Update sdk/testing/stepwise/environments/docker/environment.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com>

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"+

I don't believe so. My editor will keep them separate if they start separate but otherwise add the named imports and sort in alpha order. Personally I like the separation, calling out imported packages that are given names:

import (
	"encoding/base64"
	"fmt"
	"os"
	"testing"

	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/sdk/helper/keysutil"
	"github.com/hashicorp/vault/sdk/testing/stepwise"
	"github.com/mitchellh/mapstructure"

	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"
)

vs.

import (
	"encoding/base64"
	"fmt"
	"os"
	"testing"

	"github.com/hashicorp/vault/api"
	"github.com/hashicorp/vault/sdk/helper/keysutil"
	"github.com/hashicorp/vault/sdk/testing/stepwise"
	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"
	"github.com/mitchellh/mapstructure"
)

That said, I'm fine accepting the suggestions and having them in with the others, I just think the separation provides more clarity.

catsby

comment created time in 10 days

push eventhashicorp/vault

Clint

commit sha a7248e5c97872333278a264f533aa307a3b95699

Update builtin/logical/transit/stepwise_test.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com>

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha e9eba5a4e8b4ed1a64bf0c8f576c5c6b3b871938

Update builtin/logical/aws/stepwise_test.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com>

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha 45c22195b46964fce144d1de9013dc15f8ecdd1e

Update builtin/credential/userpass/stepwise_test.go Co-authored-by: Calvin Leung Huang <cleung2010@gmail.com>

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken

I don't believe so; it's only set once in the Initialize method which is only called once. From there, it's private and read but not set again

catsby

comment created time in 10 days

push eventhashicorp/vault

Clint

commit sha 5ee44cc84462f3b0f68db110174574b23a089b69

Update sdk/testing/stepwise/environments/docker/environment.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()

Added panic in 1292a0262

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 1292a026245890de6fca7e3c9a76f7cc9a248a90

panic if GenerateUUID returns an error

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}

I updated how this was handled in f7bec5ef6 , it's similar to your suggestion

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha f7bec5ef66e6525b1b6b2e40451a237e8ddc837c

update error handling / collection in Teardown

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	// networkID tracks the network ID of the created docker network+	networkID string++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	//clean up networks+	if rc.networkID != "" {+		cli, err := docker.NewClientWithOpts(docker.FromEnv, docker.WithVersion("1.40"))+		if err != nil {+			return err+		}+		if err := cli.NetworkRemove(context.Background(), rc.networkID); err != nil {+			return err+		}+	}++	return result

I applied this suggestion, but for whatever reason the UI is still showing this. I'm going to close this, it was applied in ae29f44e6

catsby

comment created time in 10 days

push eventhashicorp/vault

Clint

commit sha ae29f44e618fc8b30d14ec367f76a6e396b73edd

Update sdk/testing/stepwise/environments/docker/environment.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package transit++import (+	"encoding/base64"+	"fmt"+	"os"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/keysutil"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"github.com/mitchellh/mapstructure"++	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++// TestBackend_basic_docker is an example test using the Docker Environment+func TestAccBackend_basic_docker(t *testing.T) {+	decryptData := make(map[string]interface{})+	envOptions := stepwise.MountOptions{+		RegistryName:    "updatedtransit",+		PluginType:      stepwise.PluginTypeSecrets,+		PluginName:      "transit",+		MountPathPrefix: "transit_temp",+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment("updatedtransit", &envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseListPolicy(t, "test", true),+			testAccStepwiseWritePolicy(t, "test", true),+			testAccStepwiseListPolicy(t, "test", false),+			testAccStepwiseReadPolicy(t, "test", false, true),+			testAccStepwiseEncryptContext(t, "test", testPlaintext, "my-cool-context", decryptData),+			testAccStepwiseDecrypt(t, "test", testPlaintext, decryptData),+			testAccStepwiseEnableDeletion(t, "test"),+			testAccStepwiseDeletePolicy(t, "test"),+			testAccStepwiseReadPolicy(t, "test", true, true),+		},+	})+}++func testAccStepwiseWritePolicy(t *testing.T, name string, derived bool) stepwise.Step {+	ts := stepwise.Step{+		Operation: stepwise.WriteOperation,+		Path:      "keys/" + name,+		Data: map[string]interface{}{+			"derived": derived,+		},+	}+	if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+		ts.Data["type"] = "chacha20-poly1305"+	}+	return ts+}++func testAccStepwiseListPolicy(t *testing.T, name string, expectNone bool) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ListOperation,+		Path:      "keys",+		Assert: func(resp *api.Secret, err error) error {+			if (resp == nil || len(resp.Data) == 0) && !expectNone {+				return fmt.Errorf("missing response")+			}+			if expectNone && resp != nil {+				return fmt.Errorf("response data when expecting none")+			}++			if expectNone && resp == nil {+				return nil+			}++			var d struct {+				Keys []string `mapstructure:"keys"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}+			if len(d.Keys) > 0 && d.Keys[0] != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if len(d.Keys) != 1 {+				return fmt.Errorf("only 1 key expected, %d returned", len(d.Keys))+			}+			return nil+		},+	}+}++func testAccStepwiseReadPolicy(t *testing.T, name string, expectNone, derived bool) stepwise.Step {+	t.Helper()+	return testAccStepwiseReadPolicyWithVersions(t, name, expectNone, derived, 1, 0)+}++func testAccStepwiseReadPolicyWithVersions(t *testing.T, name string, expectNone, derived bool, minDecryptionVersion int, minEncryptionVersion int) stepwise.Step {+	t.Helper()+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      "keys/" + name,+		Assert: func(resp *api.Secret, err error) error {+			t.Helper()+			if resp == nil && !expectNone {+				return fmt.Errorf("missing response")+			} else if expectNone {+				if resp != nil {+					return fmt.Errorf("response when expecting none")+				}+				return nil+			}+			var d struct {+				Name                 string           `mapstructure:"name"`+				Key                  []byte           `mapstructure:"key"`+				Keys                 map[string]int64 `mapstructure:"keys"`+				Type                 string           `mapstructure:"type"`+				Derived              bool             `mapstructure:"derived"`+				KDF                  string           `mapstructure:"kdf"`+				DeletionAllowed      bool             `mapstructure:"deletion_allowed"`+				ConvergentEncryption bool             `mapstructure:"convergent_encryption"`+				MinDecryptionVersion int              `mapstructure:"min_decryption_version"`+				MinEncryptionVersion int              `mapstructure:"min_encryption_version"`+			}+			if err := mapstructure.Decode(resp.Data, &d); err != nil {+				return err+			}++			if d.Name != name {+				return fmt.Errorf("bad name: %#v", d)+			}+			if os.Getenv("TRANSIT_ACC_KEY_TYPE") == "CHACHA" {+				if d.Type != keysutil.KeyType(keysutil.KeyType_ChaCha20_Poly1305).String() {+					return fmt.Errorf("bad key type: %#v", d)+				}+			} else if d.Type != keysutil.KeyType(keysutil.KeyType_AES256_GCM96).String() {+				return fmt.Errorf("bad key type: %#v", d)+			}+			// Should NOT get a key back+			if d.Key != nil {+				return fmt.Errorf("bad: %#v", d)

I updated the error messages for clarity in 3672dd9bf

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 3672dd9bf55a6ddb18dec35b068f30080139fd3e

update error messages to provide clarity

view details

push time in 10 days

push eventhashicorp/vault

catsby

commit sha 17efb8577cf44014cc4d2fbdc3df568f252d411a

fix typos

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha c0cd87f738272b12723b5864610997abf1cbb9e2

Update builtin/logical/transit/stepwise_test.go Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha 82126541e64d6593b8e06109e41e344aecdfabeb

Update builtin/logical/aws/stepwise_test.go `test` -> `testFunc` Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha 93ed36c3c02561f97b9fa5a6c461828be79f31b5

Update builtin/credential/userpass/stepwise_test.go More explicit error checking and responding Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(+	t *testing.T, name string, password string, policies string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.UpdateOperation,+		Path:      "users/" + name,+		Data: map[string]interface{}{+			"password": password,+			"policies": policies,+		},+	}+}++func testAccStepwiseDeleteUser(t *testing.T, n string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.DeleteOperation,+		Path:      "users/" + n,+	}+}++func testAccStepwiseReadUser(t *testing.T, name string, policies string) stepwise.Step {+	return stepwise.Step{+		Operation: stepwise.ReadOperation,+		Path:      "users/" + name,+		Assert: func(resp *api.Secret, err error) error {+			if resp == nil {+				if policies == "" {+					return nil+				}++				return fmt.Errorf("bad: %#v", resp)

Updated in 938e9aed2

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 938e9aed2b1ba4e0f91d3b8c90ba9b0e4bb0023e

update error message in test

view details

push time in 10 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{

This is an example test copied/reproduced from an existing test. The purpose is to demonstrate using Stepwise and provide a comparable test to verify the results are equivalent. I'm not opposed to makes these tests better but for the sake of this PR I'm going to ask we defer those changes.

catsby

comment created time in 10 days

push eventhashicorp/vault

catsby

commit sha 77fbfa10352bd931e0f07490fac242d038abb57b

fix redundant name

view details

push time in 10 days

push eventhashicorp/vault

Clint

commit sha b37910f726524b165425d4f8a99546317b5ea724

Update sdk/testing/stepwise/environments/docker/environment.go Great catch, thanks Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 11 days

push eventhashicorp/vault

Clint

commit sha 2cc9606bd5d270c0aa97506031bb2dc1ba2f0949

Update sdk/testing/stepwise/environments/docker/environment.go haha thanks :D Co-authored-by: Michael Golowka <72365+pcman312@users.noreply.github.com>

view details

push time in 11 days

push eventhashicorp/vault

Jason O'Donnell

commit sha be7f1692309fad4988acd5bd1d02536ac3220acf

plugins/ssh: add diabled host key verification warning (#9304)

view details

Jason O'Donnell

commit sha fff0db3b2a1de5f122f9ff9ffe5e8230b9c7b8ac

plugins/openldap: update go mod (#9301) * plugins/openldap: update go mod * go mod vendor

view details

Noelle Daley

commit sha 8d2e829918ff16123b5438d5b9c5999c63b45f84

Ui/fix manage grid (#9309) * fix typo in known_primary_cluster_addrs * make height of manage actions the same on all browsers

view details

Jim Kalafut

commit sha 90689b43c4fb35be839154cf18fc334e0790fc15

Update kerberos auth plugin (#9307)

view details

Austin Gebauer

commit sha a5002ea5041df3d1dca9032ef4c128782bcd7090

changelog++

view details

catsby

commit sha e38f6dbef10d4ada0a0b638c7049ce0182e3d447

Merge branch 'master' into stepwise-docker-env * master: changelog++ Update kerberos auth plugin (#9307) Ui/fix manage grid (#9309) plugins/openldap: update go mod (#9301) plugins/ssh: add diabled host key verification warning (#9304)

view details

catsby

commit sha f487aa7227e360a4a2bc9e4687aa100cac85a5e4

vendor stepwise

view details

push time in 11 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package userpass++import (+	"fmt"+	"reflect"+	"testing"++	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/sdk/helper/policyutil"+	"github.com/mitchellh/mapstructure"++	"github.com/hashicorp/vault/sdk/testing/stepwise"+	dockerEnvironment "github.com/hashicorp/vault/sdk/testing/stepwise/environments/docker"+)++func TestAccBackend_stepwise_UserCrud(t *testing.T) {+	customPluginName := "my-userpass"+	envOptions := &stepwise.MountOptions{+		RegistryName:    customPluginName,+		PluginType:      stepwise.PluginTypeCredential,+		PluginName:      "userpass",+		MountPathPrefix: customPluginName,+	}+	stepwise.Run(t, stepwise.Case{+		Environment: dockerEnvironment.NewEnvironment(customPluginName, envOptions),+		Steps: []stepwise.Step{+			testAccStepwiseUser(t, "web", "password", "foo"),+			testAccStepwiseReadUser(t, "web", "foo"),+			testAccStepwiseDeleteUser(t, "web"),+			testAccStepwiseReadUser(t, "web", ""),+		},+	})+}++func testAccStepwiseUser(+	t *testing.T, name string, password string, policies string) stepwise.Step {

🤔 I'm going to defer this for now, I'm not seeing this method used often, just this test. Am I mistaken?

catsby

comment created time in 12 days

Pull request review commenthashicorp/vault

Stepwise docker env

+package docker++import (+	"context"+	"crypto/ecdsa"+	"crypto/elliptic"+	"crypto/rand"+	"crypto/tls"+	"crypto/x509"+	"crypto/x509/pkix"+	"encoding/hex"+	"encoding/json"+	"encoding/pem"+	"errors"+	"fmt"+	"io/ioutil"+	"math/big"+	mathrand "math/rand"+	"net"+	"net/http"+	"os"+	"path"+	"path/filepath"+	"strings"+	"testing"+	"time"++	"github.com/docker/docker/api/types"+	"github.com/docker/docker/api/types/container"+	"github.com/docker/docker/api/types/network"+	"github.com/docker/go-connections/nat"+	"github.com/hashicorp/go-cleanhttp"+	"github.com/hashicorp/go-multierror"+	"github.com/hashicorp/vault/api"+	"github.com/hashicorp/vault/internalshared/reloadutil"+	"github.com/hashicorp/vault/sdk/helper/consts"+	"github.com/hashicorp/vault/sdk/testing/stepwise"+	"golang.org/x/net/http2"++	docker "github.com/docker/docker/client"+	uuid "github.com/hashicorp/go-uuid"+)++var _ stepwise.Environment = &DockerCluster{}++// DockerCluster is used to managing the lifecycle of the test Vault cluster+type DockerCluster struct {+	// PluginName is the input from the test case+	PluginName string+	// ClusterName is a UUID name of the cluster.+	CluterName string++	// MountOptions are a set of options for registering and mounting the plugin+	MountOptions stepwise.MountOptions++	RaftStorage   bool+	CACertBytes   []byte+	CACertPEM     []byte+	CAKeyPEM      []byte+	CACertPEMFile string+	ID            string+	ClusterName   string+	RootCAs       *x509.CertPool+	CACert        *x509.Certificate+	CAKey         *ecdsa.PrivateKey+	CleanupFunc   func()+	SetupFunc     func()+	ClusterNodes  []*DockerClusterNode++	barrierKeys  [][]byte+	recoveryKeys [][]byte+	tmpDir       string++	clientAuthRequired bool+	// the mountpath of the plugin under test+	mountPath string+	// rootToken is the initial root token created when the Vault cluster is+	// created.+	rootToken string+}++// Teardown stops all the containers.+func (rc *DockerCluster) Teardown() error {+	var result error+	for _, node := range rc.ClusterNodes {+		if err := node.Cleanup(); err != nil {+			result = multierror.Append(result, err)+		}+	}++	return result+}++// MountPath returns the path that the plugin under test is mounted at. If a+// MountPathPrefix was given, the mount path uses the prefix with a uuid+// appended. The default is the given PluginName with a uuid suffix.+func (dc *DockerCluster) MountPath() string {+	if dc.mountPath != "" {+		return dc.mountPath+	}++	uuidStr, _ := uuid.GenerateUUID()+	prefix := dc.PluginName+	if dc.MountOptions.MountPathPrefix != "" {+		prefix = dc.MountOptions.MountPathPrefix+	}++	dc.mountPath = fmt.Sprintf("%s_%s", prefix, uuidStr)+	if dc.MountOptions.PluginType == stepwise.PluginTypeCredential {+		dc.mountPath = fmt.Sprintf("%s/%s", "auth", dc.mountPath)+	}++	return dc.mountPath+}++// RootToken returns the root token of the cluster, if set+func (dc *DockerCluster) RootToken() string {+	return dc.rootToken+}++// Name returns the name of this environment+func (dc *DockerCluster) Name() string {+	return "Docker"+}++// Client returns a clone of the configured Vault API client.+func (dc *DockerCluster) Client() (*api.Client, error) {+	if len(dc.ClusterNodes) > 0 {+		if dc.ClusterNodes[0].Client != nil {+			c, err := dc.ClusterNodes[0].Client.Clone()+			if err != nil {+				return nil, err+			}+			c.SetToken(dc.ClusterNodes[0].Client.Token())+			return c, nil+		}+	}++	return nil, errors.New("no configured client found")+}++func (n *DockerClusterNode) Name() string {+	return n.Cluster.ClusterName + "-" + n.NodeID+}++func (rc *DockerCluster) Initialize(ctx context.Context) error {+	client, err := rc.ClusterNodes[0].CreateAPIClient()+	if err != nil {+		return err+	}++	var resp *api.InitResponse+	for ctx.Err() == nil {+		resp, err = client.Sys().Init(&api.InitRequest{+			SecretShares:    3,+			SecretThreshold: 3,+		})+		if err == nil && resp != nil {+			break+		}+		time.Sleep(500 * time.Millisecond)+	}+	if err != nil {+		return err+	}+	if resp == nil {+		return fmt.Errorf("nil response to init request")+	}++	for _, k := range resp.Keys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.barrierKeys = append(rc.barrierKeys, raw)+	}++	for _, k := range resp.RecoveryKeys {+		raw, err := hex.DecodeString(k)+		if err != nil {+			return err+		}+		rc.recoveryKeys = append(rc.recoveryKeys, raw)+	}++	rc.rootToken = resp.RootToken++	ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)+	defer cancel()++	// Unseal+	for j, node := range rc.ClusterNodes {+		// copy the index value, so we're not reusing it in deeper scopes+		i := j+		client, err := node.CreateAPIClient()+		if err != nil {+			return err+		}+		node.Client = client++		if i > 0 && rc.RaftStorage {+			leader := rc.ClusterNodes[0]+			resp, err := client.Sys().RaftJoin(&api.RaftJoinRequest{+				LeaderAPIAddr:    fmt.Sprintf("https://%s:%d", rc.ClusterNodes[0].Name(), leader.Address.Port),+				LeaderCACert:     string(rc.CACertPEM),+				LeaderClientCert: string(node.ServerCertPEM),+				LeaderClientKey:  string(node.ServerKeyPEM),+			})+			if err != nil {+				return err+			}+			if resp == nil || !resp.Joined {+				return fmt.Errorf("nil or negative response from raft join request: %v", resp)+			}+		}++		var unsealed bool+		for _, key := range rc.barrierKeys {+			resp, err := client.Sys().Unseal(hex.EncodeToString(key))+			if err != nil {+				return err+			}+			unsealed = !resp.Sealed+		}+		if i == 0 && !unsealed {+			return fmt.Errorf("could not unseal node %d", i)+		}+		client.SetToken(rc.rootToken)++		err = testWaitHealthMatches(ctx, node.Client, func(health *api.HealthResponse) error {+			if health.Sealed {+				return fmt.Errorf("node %d is sealed: %#v", i, health)+			}+			if health.ClusterID == "" {+				return fmt.Errorf("node %d has no cluster ID", i)+			}++			rc.ID = health.ClusterID+			return nil+		})+		if err != nil {+			return err+		}++		if i == 0 {+			err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+				if !leader.IsSelf {+					return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, true)+				}++				return nil+			})+			if err != nil {+				return err+			}+		}+	}++	for i, node := range rc.ClusterNodes {+		expectLeader := i == 0+		err = testWaitLeaderMatches(ctx, node.Client, func(leader *api.LeaderResponse) error {+			if expectLeader != leader.IsSelf {+				return fmt.Errorf("node %d leader=%v, expected=%v", i, leader.IsSelf, expectLeader)+			}++			return nil+		})+		if err != nil {+			return err+		}+	}++	return nil+}++func (rc *DockerCluster) setupCA(opts *DockerClusterOptions) error {+	var err error++	certIPs := []net.IP{+		net.IPv6loopback,+		net.ParseIP("127.0.0.1"),+	}++	var caKey *ecdsa.PrivateKey+	if opts != nil && opts.CAKey != nil {+		caKey = opts.CAKey+	} else {+		caKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+		if err != nil {+			return err+		}+	}+	rc.CAKey = caKey++	var caBytes []byte+	if opts != nil && len(opts.CACert) > 0 {+		caBytes = opts.CACert+	} else {+		CACertTemplate := &x509.Certificate{+			Subject: pkix.Name{+				CommonName: "localhost",+			},+			DNSNames:              []string{"localhost"},+			IPAddresses:           certIPs,+			KeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,+			SerialNumber:          big.NewInt(mathrand.Int63()),+			NotBefore:             time.Now().Add(-30 * time.Second),+			NotAfter:              time.Now().Add(262980 * time.Hour),+			BasicConstraintsValid: true,+			IsCA:                  true,+		}+		caBytes, err = x509.CreateCertificate(rand.Reader, CACertTemplate, CACertTemplate, caKey.Public(), caKey)+		if err != nil {+			return err+		}+	}+	CACert, err := x509.ParseCertificate(caBytes)+	if err != nil {+		return err+	}+	rc.CACert = CACert+	rc.CACertBytes = caBytes++	rc.RootCAs = x509.NewCertPool()+	rc.RootCAs.AddCert(CACert)++	CACertPEMBlock := &pem.Block{+		Type:  "CERTIFICATE",+		Bytes: caBytes,+	}+	rc.CACertPEM = pem.EncodeToMemory(CACertPEMBlock)++	rc.CACertPEMFile = filepath.Join(rc.tmpDir, "ca", "ca.pem")+	err = ioutil.WriteFile(rc.CACertPEMFile, rc.CACertPEM, 0755)+	if err != nil {+		return err+	}++	marshaledCAKey, err := x509.MarshalECPrivateKey(caKey)+	if err != nil {+		return err+	}+	CAKeyPEMBlock := &pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledCAKey,+	}+	rc.CAKeyPEM = pem.EncodeToMemory(CAKeyPEMBlock)++	return nil+}++// Don't call this until n.Address.IP is populated+func (n *DockerClusterNode) setupCert() error {+	var err error++	n.ServerKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)+	if err != nil {+		return err+	}++	certTemplate := &x509.Certificate{+		Subject: pkix.Name{+			CommonName: n.Name(),+		},+		DNSNames:    []string{"localhost", n.Name()},+		IPAddresses: []net.IP{net.IPv6loopback, net.ParseIP("127.0.0.1")},+		ExtKeyUsage: []x509.ExtKeyUsage{+			x509.ExtKeyUsageServerAuth,+			x509.ExtKeyUsageClientAuth,+		},+		KeyUsage:     x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageKeyAgreement,+		SerialNumber: big.NewInt(mathrand.Int63()),+		NotBefore:    time.Now().Add(-30 * time.Second),+		NotAfter:     time.Now().Add(262980 * time.Hour),+	}+	n.ServerCertBytes, err = x509.CreateCertificate(rand.Reader, certTemplate, n.Cluster.CACert, n.ServerKey.Public(), n.Cluster.CAKey)+	if err != nil {+		return err+	}+	n.ServerCert, err = x509.ParseCertificate(n.ServerCertBytes)+	if err != nil {+		return err+	}+	n.ServerCertPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "CERTIFICATE",+		Bytes: n.ServerCertBytes,+	})++	marshaledKey, err := x509.MarshalECPrivateKey(n.ServerKey)+	if err != nil {+		return err+	}+	n.ServerKeyPEM = pem.EncodeToMemory(&pem.Block{+		Type:  "EC PRIVATE KEY",+		Bytes: marshaledKey,+	})++	n.ServerCertPEMFile = filepath.Join(n.WorkDir, "cert.pem")+	err = ioutil.WriteFile(n.ServerCertPEMFile, n.ServerCertPEM, 0755)+	if err != nil {+		return err+	}++	n.ServerKeyPEMFile = filepath.Join(n.WorkDir, "key.pem")+	err = ioutil.WriteFile(n.ServerKeyPEMFile, n.ServerKeyPEM, 0755)+	if err != nil {+		return err+	}++	tlsCert, err := tls.X509KeyPair(n.ServerCertPEM, n.ServerKeyPEM)+	if err != nil {+		return err+	}++	certGetter := reloadutil.NewCertificateGetter(n.ServerCertPEMFile, n.ServerKeyPEMFile, "")+	if err := certGetter.Reload(); err != nil {+		return err+	}+	tlsConfig := &tls.Config{+		Certificates:   []tls.Certificate{tlsCert},+		RootCAs:        n.Cluster.RootCAs,+		ClientCAs:      n.Cluster.RootCAs,+		ClientAuth:     tls.RequestClientCert,+		NextProtos:     []string{"h2", "http/1.1"},+		GetCertificate: certGetter.GetCertificate,+	}++	if n.Cluster.clientAuthRequired {+		tlsConfig.ClientAuth = tls.RequireAndVerifyClientCert+	}+	n.TLSConfig = tlsConfig++	return nil+}++// DockerClusterNode represents a single instance of Vault in a cluster+type DockerClusterNode struct {+	NodeID            string+	Address           *net.TCPAddr+	HostPort          string+	Client            *api.Client+	ServerCert        *x509.Certificate+	ServerCertBytes   []byte+	ServerCertPEM     []byte+	ServerCertPEMFile string+	ServerKey         *ecdsa.PrivateKey+	ServerKeyPEM      []byte+	ServerKeyPEMFile  string+	TLSConfig         *tls.Config+	WorkDir           string+	Cluster           *DockerCluster+	container         *types.ContainerJSON+	dockerAPI         *docker.Client+}++// CreateAPIClient creates and configures a Vault API client to communicate with+// the running Vault Cluster for this DockerClusterNode+func (n *DockerClusterNode) CreateAPIClient() (*api.Client, error) {+	transport := cleanhttp.DefaultPooledTransport()+	transport.TLSClientConfig = n.TLSConfig.Clone()+	if err := http2.ConfigureTransport(transport); err != nil {+		return nil, err+	}+	client := &http.Client{+		Transport: transport,+		CheckRedirect: func(*http.Request, []*http.Request) error {+			// This can of course be overridden per-test by using its own client+			return fmt.Errorf("redirects not allowed in these tests")+		},+	}+	config := api.DefaultConfig()+	if config.Error != nil {+		return nil, config.Error+	}+	config.Address = fmt.Sprintf("https://127.0.0.1:%s", n.HostPort)+	config.HttpClient = client+	config.MaxRetries = 0+	apiClient, err := api.NewClient(config)+	if err != nil {+		return nil, err+	}+	apiClient.SetToken(n.Cluster.RootToken())+	return apiClient, nil+}++// Cleanup kills the container of the node+func (n *DockerClusterNode) Cleanup() error {+	return n.dockerAPI.ContainerKill(context.Background(), n.container.ID, "KILL")+}++func (n *DockerClusterNode) Start(cli *docker.Client, caDir, netName string, netCIDR *DockerClusterNode, pluginBinPath string) error {+	n.dockerAPI = cli++	err := n.setupCert()+	if err != nil {+		return err+	}++	vaultCfg := map[string]interface{}{+		"listener": map[string]interface{}{+			"tcp": map[string]interface{}{+				"address":       fmt.Sprintf("%s:%d", "0.0.0.0", 8200),+				"tls_cert_file": "/vault/config/cert.pem",+				"tls_key_file":  "/vault/config/key.pem",+				"telemetry": map[string]interface{}{+					"unauthenticated_metrics_access": true,+				},+			},+		},+		"telemetry": map[string]interface{}{+			"disable_hostname": true,+		},+		"storage": map[string]interface{}{+			"raft": map[string]interface{}{+				"path":    "/vault/file",+				"node_id": n.NodeID,+			},+		},+		"cluster_name":         netName,+		"log_level":            "TRACE",+		"raw_storage_endpoint": true,+		"plugin_directory":     "/vault/config",+		// disable_mlock is required for working in the Docker environment with+		// custom plugins+		"disable_mlock": true,+		// These are being provided by docker-entrypoint now, since we don't know+		// the address before the container starts.+		//"api_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port),+		//"cluster_addr": fmt.Sprintf("https://%s:%d", n.Address.IP, n.Address.Port+1),+	}+	cfgJSON, err := json.Marshal(vaultCfg)+	if err != nil {+		return err+	}++	err = ioutil.WriteFile(filepath.Join(n.WorkDir, "local.json"), cfgJSON, 0644)+	if err != nil {+		return err+	}+	// setup plugin bin copy if needed+	copyFromTo := map[string]string{+		n.WorkDir: "/vault/config",+		caDir:     "/usr/local/share/ca-certificates/",+	}+	if pluginBinPath != "" {+		base := path.Base(pluginBinPath)+		copyFromTo[pluginBinPath] = filepath.Join("/vault/config", base)+	}++	r := &Runner{+		dockerAPI: cli,+		ContainerConfig: &container.Config{+			Image: "vault",+			Entrypoint: []string{"/bin/sh", "-c", "update-ca-certificates && " ++				"exec /usr/local/bin/docker-entrypoint.sh vault server -log-level=trace -dev-plugin-dir=./vault/config -config /vault/config/local.json"},+			Env: []string{+				"VAULT_CLUSTER_INTERFACE=eth0",+				"VAULT_API_ADDR=https://127.0.0.1:8200",+				fmt.Sprintf("VAULT_REDIRECT_ADDR=https://%s:8200", n.Name()),+			},+			Labels:       nil,+			ExposedPorts: nat.PortSet{"8200/tcp": {}, "8201/tcp": {}},+		},+		ContainerName: n.Name(),+		NetName:       netName,+		CopyFromTo:    copyFromTo,+	}++	n.container, err = r.Start(context.Background())+	if err != nil {+		return err+	}++	n.Address = &net.TCPAddr{+		IP:   net.ParseIP(n.container.NetworkSettings.IPAddress),+		Port: 8200,+	}+	ports := n.container.NetworkSettings.NetworkSettingsBase.Ports[nat.Port("8200/tcp")]+	if len(ports) == 0 {+		n.Cleanup()+		return fmt.Errorf("could not find port binding for 8200/tcp")+	}+	n.HostPort = ports[0].HostPort++	return nil+}++// DockerClusterOptions has options for setting up the docker cluster+type DockerClusterOptions struct {+	KeepStandbysSealed bool+	RequireClientAuth  bool+	SkipInit           bool+	CACert             []byte+	NumCores           int+	tmpDir             string+	PluginTestBin      string+	// SetupFunc is called after the cluster is started.+	SetupFunc func(t testing.T, c *DockerCluster)+	CAKey     *ecdsa.PrivateKey+}++//+// test methods/functions+//++// testWaitHealthMatches checks health+func testWaitHealthMatches(ctx context.Context, client *api.Client, ready func(response *api.HealthResponse) error) error {+	var health *api.HealthResponse+	var err error+	for ctx.Err() == nil {+		health, err = client.Sys().Health()+		switch {+		case err != nil:+		case health == nil:+			err = fmt.Errorf("nil response to health check")+		default:+			err = ready(health)+			if err == nil {+				return nil+			}+		}+		time.Sleep(500 * time.Millisecond)+	}+	return fmt.Errorf("error checking health: %v", err)+}++func testWaitLeaderMatches(ctx context.Context, client *api.Client, ready func(response *api.LeaderResponse) error) error {+	var leader *api.LeaderResponse+	var err error+	for ctx.Err() == nil {+		leader, err = client.Sys().Leader()+		switch {+		case err != nil:+		case leader == nil:+			err = fmt.Errorf("nil response to leader check")+		default:+			err = ready(leader)+			if err == nil {+				return nil+			}+		}+		time.Sleep(500 * time.Millisecond)+	}+	return fmt.Errorf("error checking leader: %v", err)+}++// end test helper methods++// TODO: allow number of cores/servers to be configurable+var DefaultNumCores = 1++// creates a managed docker container running Vault+func (cluster *DockerCluster) setupDockerCluster(opts *DockerClusterOptions) error {+	if opts != nil && opts.tmpDir != "" {+		if _, err := os.Stat(opts.tmpDir); os.IsNotExist(err) {+			if err := os.MkdirAll(opts.tmpDir, 0700); err != nil {+				return err+			}+		}+		cluster.tmpDir = opts.tmpDir+	} else {+		tempDir, err := ioutil.TempDir("", "vault-test-cluster-")+		if err != nil {+			return err+		}+		cluster.tmpDir = tempDir+	}+	caDir := filepath.Join(cluster.tmpDir, "ca")+	if err := os.MkdirAll(caDir, 0755); err != nil {+		return err+	}++	var numCores int+	if opts == nil || opts.NumCores == 0 {+		numCores = DefaultNumCores+	} else {+		numCores = opts.NumCores+	}++	if opts != nil && opts.RequireClientAuth {+		cluster.clientAuthRequired = true+	}++	cidr := "192.168.128.0/20"

I ended up removing this; we no longer specify the CIDR and let Docker do this on it's own. We also generate a UUID for the network name, and clean up the network during Teardown in bb300a30a

catsby

comment created time in 12 days

more