[VAULT-39158, VAULT-39159]pipeline: add support for building HVD images (#9012) (#9130)

* [VAULT-39159]: pipeline: add support for querying HCP image service

In order to facilitate testing Vault Enterprise directly in HCP we need
tools to both request an image be built from a candidate build and to
also wait for the image to be available in order to execute test
scenarios with it. This PR adds a few new `pipeline` sub-commands that
can will be used for this purpose.

`pipeline github find workflow-artifact` can be used to find the path of
an artifact that matches the given filter criteria. You'll need to
provide a pull request number, workflow name, and either an exact
artifact name or a pattern. When providing a pattern only the first
match will be returned so make sure your regular expression is robust.

`pipeline hcp get image` will return the image information for an HCP
image. You will need to supply auth via the `HCP_USERNAME` and
`HCP_PASSWORD` environment variables in order to query the image
service. It also takes an enviroment flag so you can query the image
service in different environments.

`pipeline hcp wait image` is like `pipeline hcp get image` except that
it will continue to retry for a given timeout and with a given delay
between requests. In this way it can be used to wait for an image to be
available.

As part of this we also update our Go modules to the latest versions
that are compatible.



* [VAULT-39158]: actions(build-hcp-image): add workflow for building HCP images



* copywrite: add missing headers



* remove unused output



* address feedback



* allow prerelease artifacts



---------

Signed-off-by: Ryan Cragun <me@ryan.ec>
Co-authored-by: Ryan Cragun <me@ryan.ec>
This commit is contained in:
Vault Automation 2025-09-04 17:20:25 -06:00 committed by GitHub
parent 1636e247b2
commit 51f56b8536
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1249 additions and 130 deletions

View File

@ -4,14 +4,15 @@ go 1.23.2
require (
github.com/Masterminds/semver v1.5.0
github.com/google/go-github/v68 v68.0.0
github.com/hashicorp/hcl/v2 v2.23.0
github.com/hashicorp/releases-api v0.2.1
github.com/jedib0t/go-pretty/v6 v6.6.7
github.com/avast/retry-go/v4 v4.6.1
github.com/google/go-github/v74 v74.0.0
github.com/hashicorp/hcl/v2 v2.24.0
github.com/hashicorp/releases-api v0.2.3
github.com/jedib0t/go-pretty/v6 v6.6.8
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
github.com/veqryn/slog-context v0.7.0
github.com/zclconf/go-cty v1.16.2
github.com/veqryn/slog-context v0.8.0
github.com/zclconf/go-cty v1.16.4
)
require (
@ -21,11 +22,11 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.1 // indirect
github.com/go-openapi/jsonpointer v0.21.1 // indirect
github.com/go-openapi/errors v0.22.2 // indirect
github.com/go-openapi/jsonpointer v0.21.2 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
@ -52,21 +53,22 @@ require (
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/pointerstructure v1.2.1 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/oklog/ulid/v2 v2.1.0 // indirect
github.com/oklog/ulid/v2 v2.1.1 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.6 // indirect
go.mongodb.org/mongo-driver v1.17.3 // indirect
github.com/spf13/pflag v1.0.7 // indirect
go.mongodb.org/mongo-driver v1.17.4 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.35.0 // indirect
go.opentelemetry.io/otel/metric v1.35.0 // indirect
go.opentelemetry.io/otel/trace v1.35.0 // indirect
golang.org/x/mod v0.24.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/sync v0.12.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/tools v0.31.0 // indirect
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
golang.org/x/tools v0.36.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@ -22,8 +22,10 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU=
github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/avast/retry-go/v4 v4.6.1 h1:VkOLRubHdisGrHnTu89g08aQEWEgRU7LVEop3GbIcMk=
github.com/avast/retry-go/v4 v4.6.1/go.mod h1:V6oF8njAwxJ5gRo1Q7Cxab24xs5NCWZBeaHHBklR8mA=
github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk=
github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@ -43,16 +45,16 @@ github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/
github.com/go-jose/go-jose/v3 v3.0.4 h1:Wp5HA7bLQcKnf6YYao/4kpRpVMp/yf6+pJKV8WFSaNY=
github.com/go-jose/go-jose/v3 v3.0.4/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU=
github.com/go-openapi/errors v0.22.1/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic=
github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/errors v0.22.2 h1:rdxhzcBUazEcGccKqbY1Y7NS8FDcMyIRr0934jrYnZg=
github.com/go-openapi/errors v0.22.2/go.mod h1:+n/5UdIqdVnLIJ6Q9Se8HNGUXYaY6CN8ImWzfi/Gzp0=
github.com/go-openapi/jsonpointer v0.21.2 h1:AqQaNADVwq/VnkCmQg6ogE+M3FOsKTytwges0JdwVuA=
github.com/go-openapi/jsonpointer v0.21.2/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
@ -74,8 +76,8 @@ github.com/golang-migrate/migrate/v4 v4.14.1/go.mod h1:l7Ks0Au6fYHuUIxUhQ0rcVX1u
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-github/v68 v68.0.0 h1:ZW57zeNZiXTdQ16qrDiZ0k6XucrxZ2CGmoTvcCyQG6s=
github.com/google/go-github/v68 v68.0.0/go.mod h1:K9HAUBovM2sLwM408A18h+wd9vqdLOEqTUCbnRIcx68=
github.com/google/go-github/v74 v74.0.0 h1:yZcddTUn8DPbj11GxnMrNiAnXH14gNs559AsUpNpPgM=
github.com/google/go-github/v74 v74.0.0/go.mod h1:ubn/YdyftV80VPSI26nSJvaEsTOnsjrxG3o9kJhcyak=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -90,10 +92,10 @@ github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/C
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl/v2 v2.23.0 h1:Fphj1/gCylPxHutVSEOf2fBOh1VE4AuLV7+kbJf3qos=
github.com/hashicorp/hcl/v2 v2.23.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
github.com/hashicorp/releases-api v0.2.1 h1:c2I97uYmhjCHIYy4OxfTy406jlrm/CZAYPpbKwA0pMs=
github.com/hashicorp/releases-api v0.2.1/go.mod h1:2+TZWrbqji5O7NIiE/rC/8rjMCPC3oC11FNDx9A5kiM=
github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=
github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=
github.com/hashicorp/releases-api v0.2.3 h1:mwNR+lKgJtIyeSQXYGM86fZ0u8ed09v7NS2ePKmVvyc=
github.com/hashicorp/releases-api v0.2.3/go.mod h1:J8AiSwS1Qy/m/RmHskUGDu9YQRLKreBBswc6ZTY5/tI=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@ -102,14 +104,14 @@ github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451 h1:WAvSpGf7MsFuzAt
github.com/jackc/pgerrcode v0.0.0-20201024163028-a0d42d470451/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.5.4 h1:Xp2aQS8uXButQdnCMWNmvx6UysWQQC+u1EoizjguY+8=
github.com/jackc/pgx/v5 v5.5.4/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jedib0t/go-pretty/v6 v6.6.8 h1:JnnzQeRz2bACBobIaa/r+nqjvws4yEhcmaZ4n1QzsEc=
github.com/jedib0t/go-pretty/v6 v6.6.8/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -147,8 +149,8 @@ github.com/mitchellh/pointerstructure v1.2.1 h1:ZhBBeX8tSlRpu/FFhXH4RC4OJzFlqsQh
github.com/mitchellh/pointerstructure v1.2.1/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s=
github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
@ -171,8 +173,9 @@ github.com/secure-systems-lab/go-securesystemslib v0.7.0 h1:OwvJ5jQf9LnIAS83waAj
github.com/secure-systems-lab/go-securesystemslib v0.7.0/go.mod h1:/2gYnlnHVQ6xeGtfIqFy7Do03K4cdCY0A/GlJLDKLHI=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M=
github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
@ -180,53 +183,53 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.1.8 h1:FCXC1xanKO4I8plpHGH2P7koL/RzZs12l/+r7vakfm0=
github.com/tinylib/msgp v1.1.8/go.mod h1:qkpG+2ldGg4xRFmx+jfTvZPxfGFhi64BcnL9vkCm/Tw=
github.com/veqryn/slog-context v0.7.0 h1:Ne7ajlR6Mjs2rQQtpg8k0eO6krR5wzpareh5VpV+V2s=
github.com/veqryn/slog-context v0.7.0/go.mod h1:E+qpdyiQs2YKRxFnX1JjpdFE1z3Ka94Kem2q9ZG6Jjo=
github.com/zclconf/go-cty v1.16.2 h1:LAJSwc3v81IRBZyUVQDUdZ7hs3SYs9jv0eZJDWHD/70=
github.com/zclconf/go-cty v1.16.2/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/veqryn/slog-context v0.8.0 h1:lDhwAgjwx52K5StqqQzi5d0Y/F4SNyGZbsXGd8MtucM=
github.com/veqryn/slog-context v0.8.0/go.mod h1:8rsT72p0kzzN9lmkwtabIhxg7ZkpnKblt9x3Eix8Tc0=
github.com/zclconf/go-cty v1.16.4 h1:QGXaag7/7dCzb+odlGrgr+YmYZFaOCMW6DEpS+UD1eE=
github.com/zclconf/go-cty v1.16.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=
github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=
go.mongodb.org/mongo-driver v1.17.3 h1:TQyXhnsWfWtgAhMtOgtYHMTkZIfBTpMTsMnd9ZBeHxQ=
go.mongodb.org/mongo-driver v1.17.3/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw=
go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/DataDog/dd-trace-go.v1 v1.66.0 h1:025+lLubGtpiDWrRmSOxoFBPIiVRVYRcqP9oLabVOeg=
gopkg.in/DataDog/dd-trace-go.v1 v1.66.0/go.mod h1:Av6AXGmQCQAbDnwNoPiuUz1k3GS8TwQjj+vEdwmEpmM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -8,7 +8,7 @@ import (
"os"
"path/filepath"
"github.com/google/go-github/v68/github"
"github.com/google/go-github/v74/github"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/git"
"github.com/spf13/cobra"
)
@ -39,6 +39,7 @@ func newGithubCmd() *cobra.Command {
}
githubCmd.AddCommand(newGithubCopyCmd())
githubCmd.AddCommand(newGithubCreateCmd())
githubCmd.AddCommand(newGithubFindCmd())
githubCmd.AddCommand(newGithubListCmd())
githubCmd.AddCommand(newGithubSyncCmd())

View File

@ -0,0 +1,19 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"github.com/spf13/cobra"
)
func newGithubFindCmd() *cobra.Command {
findCmd := &cobra.Command{
Use: "find",
Short: "Github find commands",
Long: "Github find commands",
}
findCmd.AddCommand(newGithubFindWorkflowArtifactCmd())
return findCmd
}

View File

@ -0,0 +1,64 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"context"
"fmt"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/github"
"github.com/spf13/cobra"
)
var findWorkflowArtifact = &github.FindWorkflowArtifactReq{}
func newGithubFindWorkflowArtifactCmd() *cobra.Command {
findWorkflowArtifactCmd := &cobra.Command{
Use: "workflow-artifact [--pr 1234 --workflow build --pattern 'vault_[0-9]'",
Short: "Find an artifact associated with a pull requests workflow run",
Long: "Find an artifact associated with a pull requests workflow run",
RunE: runFindGithubWorkflowArtifactCmd,
}
findWorkflowArtifactCmd.PersistentFlags().StringVarP(&findWorkflowArtifact.ArtifactName, "name", "n", "", "The exact artifact name to match")
findWorkflowArtifactCmd.PersistentFlags().StringVarP(&findWorkflowArtifact.ArtifactPattern, "pattern", "m", "", "A pattern to match an artifact. Only the first match will be returned")
findWorkflowArtifactCmd.PersistentFlags().StringVarP(&findWorkflowArtifact.Owner, "owner", "o", "hashicorp", "The Github organization")
findWorkflowArtifactCmd.PersistentFlags().StringVarP(&findWorkflowArtifact.Repo, "repo", "r", "vault", "The Github repository. Private repositories require auth via a GITHUB_TOKEN env var")
findWorkflowArtifactCmd.PersistentFlags().IntVarP(&findWorkflowArtifact.PullNumber, "pr", "p", 0, "The pull request to use as the trigger of the workflow")
findWorkflowArtifactCmd.PersistentFlags().StringVarP(&findWorkflowArtifact.WorkflowName, "workflow", "w", "", "The name of the workflow the artifact will be associated with")
findWorkflowArtifactCmd.PersistentFlags().BoolVar(&findWorkflowArtifact.WriteToGithubOutput, "github-output", false, "Whether or not to write 'workflow-artifact' to $GITHUB_OUTPUT")
return findWorkflowArtifactCmd
}
func runFindGithubWorkflowArtifactCmd(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true // Don't spam the usage on failure
res, err := findWorkflowArtifact.Run(context.TODO(), githubCmdState.Github)
if err != nil {
return fmt.Errorf("listing github workflow failures: %w", err)
}
switch rootCfg.format {
case "json":
jsonBytes, err := res.ToJSON()
if err != nil {
return err
}
fmt.Println(string(jsonBytes))
default:
fmt.Println(res.ToTable())
}
if findWorkflowArtifact.WriteToGithubOutput {
jsonBytes, err := res.ToGithubOutput()
if err != nil {
return err
}
return writeToGithubOutput("workflow-artifact", jsonBytes)
}
return nil
}

View File

@ -0,0 +1,40 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"fmt"
"os"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/hcp"
"github.com/spf13/cobra"
)
var hcpCmdState = &struct {
client *hcp.Client
}{}
func newHCPCmd() *cobra.Command {
env := ""
hcpCmd := &cobra.Command{
Use: "hcp",
Short: "HCP commands",
Long: "HCP commands",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
hcpCmdState.client = hcp.NewClient(
hcp.WithEnvironment(hcp.Environment(env)),
hcp.WithLoadAuthFromEnv(),
)
if _, set := os.LookupEnv("HCP_PASSWORD"); !set {
fmt.Println("\x1b[1;33;49mWARNING\x1b[0m: HCP_PASSWORD has not been set. You probably want to set it and HCP_USERNAME in order to authenticate with the image service")
}
},
}
hcpCmd.AddCommand(newHCPShowCmd())
hcpCmd.AddCommand(newHCPWaitCmd())
hcpCmd.PersistentFlags().StringVarP(&env, "environment", "e", "prod", "The HCP environment to use. E.g. dev, int, prod")
return hcpCmd
}

View File

@ -0,0 +1,17 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import "github.com/spf13/cobra"
func newHCPShowCmd() *cobra.Command {
showCmd := &cobra.Command{
Use: "show",
Short: "HCP show commands",
Long: "HCP show commands",
}
showCmd.AddCommand(newHCPShowImageCmd())
return showCmd
}

View File

@ -0,0 +1,64 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"context"
"fmt"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/hcp"
"github.com/spf13/cobra"
)
var showHCPImageReq = &hcp.GetLatestProductVersionReq{}
func newHCPShowImageCmd() *cobra.Command {
availability := ""
showHCPImage := &cobra.Command{
Use: "image",
Short: "Show details of an HCP image",
Long: "Show details of an HCP image",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
showHCPImageReq.Availability = hcp.GetLatestProductVersionAvailability(availability)
},
RunE: runHCPImageShowLatestCmd,
}
showHCPImage.PersistentFlags().StringVarP(&showHCPImageReq.ProductName, "product-name", "p", "vault", "The product or component of the image")
showHCPImage.PersistentFlags().StringVarP(&showHCPImageReq.ProductVersionConstraint, "product-version-constraint", "v", "", "A comma seperated list of constraints. If left unset the latest will be returned")
showHCPImage.PersistentFlags().StringVarP(&showHCPImageReq.HostManagerVersionConstraint, "host-manager-version-constraint", "m", "", "A semver string. If left unset the latest will be used")
showHCPImage.PersistentFlags().StringVarP(&showHCPImageReq.CloudProvider, "cloud", "c", "aws", "The cloud provider you wish to search. E.g. aws, azure")
showHCPImage.PersistentFlags().StringVarP(&showHCPImageReq.CloudRegion, "region", "r", "us-west-2", "The cloud region you wish to search")
showHCPImage.PersistentFlags().StringVarP(&availability, "availability", "a", "public", "The image availability")
showHCPImage.PersistentFlags().BoolVarP(&showHCPImageReq.ExcludeReleaseCandidates, "exclude-release-candidates", "x", false, "Exclude release candidates")
return showHCPImage
}
func runHCPImageShowLatestCmd(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true // Don't spam the usage on failure
res, err := showHCPImageReq.Run(context.TODO(), hcpCmdState.client)
if err != nil {
return fmt.Errorf("showing HCP image: %w", err)
}
switch rootCfg.format {
case "json":
b, err := res.ToJSON()
if err != nil {
return err
}
fmt.Println(string(b))
case "markdown":
tbl := res.ToTable()
tbl.SetTitle("HCP Image")
fmt.Println(tbl.RenderMarkdown())
default:
fmt.Println(res.ToTable().Render())
}
return nil
}

View File

@ -0,0 +1,22 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"errors"
"github.com/spf13/cobra"
)
func newHCPWaitCmd() *cobra.Command {
waitCmd := &cobra.Command{
Use: "wait",
Short: "HCP wait commands",
Long: "HCP wait commands",
RunE: func(*cobra.Command, []string) error { return errors.New("unimplemented") },
}
waitCmd.AddCommand(newHCPWaitForImageCmd())
return waitCmd
}

View File

@ -0,0 +1,89 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package cmd
import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/hcp"
"github.com/spf13/cobra"
)
var waitForHCPImage = &hcp.WaitForImageReq{
Req: &hcp.GetLatestProductVersionReq{},
}
func newHCPWaitForImageCmd() *cobra.Command {
availability := ""
var timeout time.Duration
imageGetLatestCmd := &cobra.Command{
Use: "image",
Short: "Show details of an HCP image",
Long: "Show details of an HCP image",
PersistentPreRun: func(cmd *cobra.Command, args []string) {
waitForHCPImage.Req.Availability = hcp.GetLatestProductVersionAvailability(availability)
},
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true // Don't spam the usage on failure
ctx, cancelCause := context.WithCancelCause(context.Background())
ctx, cancel := context.WithTimeoutCause(ctx, timeout, errors.New("timed out waiting for image"))
defer cancel()
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
select {
case <-ctx.Done():
return
case s := <-c:
fmt.Printf("\x1b[1;33;49mWARNING\x1b[0m: received %s signal. Stopping now..\n", s)
cancelCause(fmt.Errorf("received signal %s", s))
cancel()
}
}()
res, err := waitForHCPImage.Run(ctx, hcpCmdState.client)
if err != nil {
return fmt.Errorf("waiting for an HCP image: %w", err)
}
switch rootCfg.format {
case "json":
b, err := res.Res.ToJSON()
if err != nil {
return err
}
fmt.Println(string(b))
case "markdown":
tbl := res.Res.ToTable()
tbl.SetTitle("HCP Image")
fmt.Println(tbl.RenderMarkdown())
default:
fmt.Println(res.Res.ToTable().Render())
}
return nil
},
}
imageGetLatestCmd.PersistentFlags().StringVarP(&waitForHCPImage.Req.ProductName, "product-name", "p", "vault", "The product or component of the image")
imageGetLatestCmd.PersistentFlags().StringVarP(&waitForHCPImage.Req.ProductVersionConstraint, "product-version-constraint", "v", "", "A comma seperated list of constraints. If left unset the latest will be returned")
imageGetLatestCmd.PersistentFlags().StringVarP(&waitForHCPImage.Req.HostManagerVersionConstraint, "host-manager-version-constraint", "m", "", "A semver string. If left unset the latest will be used")
imageGetLatestCmd.PersistentFlags().StringVarP(&waitForHCPImage.Req.CloudProvider, "cloud", "c", "aws", "The cloud provider you wish to search. E.g. aws, azure")
imageGetLatestCmd.PersistentFlags().StringVarP(&waitForHCPImage.Req.CloudRegion, "region", "r", "us-west-2", "The cloud region you wish to search")
imageGetLatestCmd.PersistentFlags().StringVarP(&availability, "availability", "a", "public", "The image availability")
imageGetLatestCmd.PersistentFlags().BoolVarP(&waitForHCPImage.Req.ExcludeReleaseCandidates, "exclude-release-candidates", "x", false, "Exclude release candidates")
imageGetLatestCmd.PersistentFlags().DurationVarP(&waitForHCPImage.Delay, "delay", "d", 10*time.Second, "the time to wait in-between requests")
imageGetLatestCmd.PersistentFlags().DurationVarP(&timeout, "timeout", "t", 30*time.Minute, "the maximum duration to wait for the image")
return imageGetLatestCmd
}

View File

@ -27,10 +27,11 @@ func newRootCmd() *cobra.Command {
}
rootCmd.PersistentFlags().StringVar(&rootCfg.logLevel, "log", "warn", "Set the log level. One of 'debug', 'info', 'warn', 'error'")
rootCmd.PersistentFlags().StringVarP(&rootCfg.format, "format", "f", "table", "The output format. Can be 'json' or 'table'")
rootCmd.PersistentFlags().StringVarP(&rootCfg.format, "format", "f", "table", "The output format. Can be 'json', 'table', and sometimes 'markdown'")
rootCmd.AddCommand(newGenerateCmd())
rootCmd.AddCommand(newGithubCmd())
rootCmd.AddCommand(newHCPCmd())
rootCmd.AddCommand(newReleasesCmd())
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
@ -51,7 +52,7 @@ func newRootCmd() *cobra.Command {
slog.SetDefault(slog.New(h))
switch rootCfg.format {
case "json", "table":
case "json", "table", "markdown":
default:
return fmt.Errorf("unsupported format: %s", rootCfg.format)
}

View File

@ -7,7 +7,7 @@ import (
"context"
"testing"
"github.com/google/go-github/v68/github"
"github.com/google/go-github/v74/github"
"github.com/stretchr/testify/require"
)

View File

@ -7,7 +7,7 @@ import (
"slices"
"strings"
gh "github.com/google/go-github/v68/github"
gh "github.com/google/go-github/v74/github"
)
type (

View File

@ -8,7 +8,7 @@ import (
"log/slog"
"slices"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
slogctx "github.com/veqryn/slog-context"
)

View File

@ -15,7 +15,7 @@ import (
"slices"
"strings"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git"
"github.com/jedib0t/go-pretty/v6/table"
slogctx "github.com/veqryn/slog-context"

View File

@ -6,7 +6,7 @@ package github
import (
"testing"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
"github.com/stretchr/testify/require"
)

View File

@ -15,7 +15,7 @@ import (
"slices"
"strings"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed"
libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/releases"

View File

@ -8,7 +8,7 @@ import (
"errors"
"testing"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/releases"
"github.com/stretchr/testify/require"

View File

@ -0,0 +1,200 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package github
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"regexp"
"slices"
gh "github.com/google/go-github/v74/github"
"github.com/jedib0t/go-pretty/v6/table"
)
// FindWorkflowArtifactReq is a request to find an artifact associated with a
// workflow run.
type FindWorkflowArtifactReq struct {
ArtifactName string
ArtifactPattern string
Owner string
PullNumber int
Repo string
WorkflowName string
WriteToGithubOutput bool
compiledPattern *regexp.Regexp
}
// FindWorkflowArtifactRes is a FindWorkflowArtifactReq response.
type FindWorkflowArtifactRes struct {
PR *gh.PullRequest `json:"pr,omitempty"`
Workflow *gh.Workflow `json:"workflow,omitempty"`
Run *WorkflowRun `json:"runs,omitempty"`
Artifact *gh.Artifact `json:"artifact,omitempty"`
}
// Run performs the search to find an artifact associated with a workflow.
func (r *FindWorkflowArtifactReq) Run(ctx context.Context, client *gh.Client) (*FindWorkflowArtifactRes, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
var err error
res := &FindWorkflowArtifactRes{}
// Validate our request. This also ensures that any pattern we've been given
// is a valid regex.
if err = r.validate(); err != nil {
return nil, fmt.Errorf("validating request: %w", err)
}
// Get the workflow details for the repo
res.Workflow, err = getWorkflow(ctx, client, r.Owner, r.Repo, r.WorkflowName)
if err != nil {
return nil, fmt.Errorf("getting workflow: %w", err)
}
// Get the pull request we're searching
res.PR, err = getPullRequest(ctx, client, r.Owner, r.Repo, r.PullNumber)
if err != nil {
return nil, fmt.Errorf("getting pull request: %w", err)
}
// Get the workflow runs associated with the workflow and the PR
opts := &gh.ListWorkflowRunsOptions{
Branch: res.PR.GetHead().GetRef(),
ExcludePullRequests: false,
HeadSHA: res.PR.GetHead().GetSHA(),
ListOptions: gh.ListOptions{PerPage: PerPageMax},
Status: "success",
}
runs, err := getWorkflowRuns(ctx, client, r.Owner, r.Repo, res.Workflow.GetID(), opts)
if err != nil {
return nil, fmt.Errorf("getting workflow runs: %w", err)
}
if len(runs) < 1 {
return nil, fmt.Errorf("no matching workflow runs are associated with the pull request: %w", err)
}
// In instances where we have more than one run we want to get the artifact
// from the most recent run if possible. Search our runs in reverse order to
// find the most recent artifact.
slices.SortFunc(runs, func(a, b *WorkflowRun) int {
return cmp.Compare(*b.Run.RunAttempt, *a.Run.RunAttempt)
})
var artifacts gh.ArtifactList
for _, run := range runs {
artifacts, err = getWorkflowRunArtifacts(ctx, client, r.Owner, r.Repo, *run.Run.ID)
if err != nil {
return nil, fmt.Errorf("getting artifacts for workflow run %d: %w", *run.Run.ID, err)
}
for _, art := range artifacts.Artifacts {
// If we've been given a name locate it by that
if r.ArtifactName != "" {
if art.GetName() == r.ArtifactName {
res.Artifact = art
return res, nil
}
} else {
// Find it by regex
if r.compiledPattern.MatchString(art.GetName()) {
res.Artifact = art
return res, nil
}
}
}
}
return nil, errors.New("unable to find artifact matching given criteria")
}
// validate ensures that we've been given the request configuration to perform
// the request.
func (r *FindWorkflowArtifactReq) validate() error {
if r == nil {
return errors.New("failed to initialize request")
}
if r.Owner == "" {
return errors.New("no github organization has been provided")
}
if r.Repo == "" {
return errors.New("no github repository has been provided")
}
if r.PullNumber == 0 {
return errors.New("no github pull request number has been provided")
}
if r.WorkflowName == "" {
return errors.New("no workflow name has been provided")
}
if r.ArtifactName == "" && r.ArtifactPattern == "" {
return errors.New("no artifact name or pattern has been provided")
}
if r.ArtifactName != "" && r.ArtifactPattern != "" {
return errors.New("you must provide only an artifact name or pattern")
}
if r.ArtifactPattern != "" {
var err error
r.compiledPattern, err = regexp.Compile(r.ArtifactPattern)
if err != nil {
return fmt.Errorf("invalid artifact pattern: %w", err)
}
}
return nil
}
// ToJSON marshals the response to JSON.
func (r *FindWorkflowArtifactRes) ToJSON() ([]byte, error) {
b, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("marshaling find workflow artifact to JSON: %w", err)
}
return b, nil
}
// ToGithubOutput marshals just the artifact response to JSON.
func (r *FindWorkflowArtifactRes) ToGithubOutput() ([]byte, error) {
b, err := json.Marshal(r.Artifact)
if err != nil {
return nil, fmt.Errorf("marshaling find workflow artifact to GITHUB_OUTPUT JSON: %w", err)
}
return b, nil
}
// ToTable marshals the response to a text table.
func (r *FindWorkflowArtifactRes) ToTable() string {
t := table.NewWriter()
t.Style().Options.DrawBorder = false
t.Style().Options.SeparateColumns = false
t.Style().Options.SeparateFooter = false
t.Style().Options.SeparateHeader = false
t.Style().Options.SeparateRows = false
t.AppendHeader(table.Row{"name", "run id", "artifact id", "url"})
t.AppendRow(table.Row{
r.Artifact.GetName(),
r.Artifact.GetWorkflowRun().GetID(),
r.Artifact.GetID(),
r.Artifact.GetArchiveDownloadURL(),
})
return t.Render()
}

View File

@ -10,7 +10,7 @@ import (
"fmt"
"strings"
gh "github.com/google/go-github/v68/github"
gh "github.com/google/go-github/v74/github"
"github.com/hashicorp/vault/tools/pipeline/internal/pkg/changed"
"github.com/jedib0t/go-pretty/v6/table"
)

View File

@ -10,7 +10,7 @@ import (
"net/http"
"sync"
gh "github.com/google/go-github/v68/github"
gh "github.com/google/go-github/v74/github"
)
// PerPageMax is the maximum number of entities to request for enpoints that
@ -75,7 +75,7 @@ func (r *ListWorkflowRunsReq) Run(ctx context.Context, client *gh.Client) (*List
return nil, fmt.Errorf("validating request: %w", err)
}
res.Workflow, err = r.getWorkflow(ctx, client)
res.Workflow, err = getWorkflow(ctx, client, r.Owner, r.Repo, r.WorkflowName)
if err != nil {
return nil, fmt.Errorf("getting workflow: %w", err)
}
@ -141,32 +141,8 @@ func (r *ListWorkflowRunsReq) validate() error {
return nil
}
// getWorkflow attempts to locate the workflow associated with our workflow name.
func (r *ListWorkflowRunsReq) getWorkflow(ctx context.Context, client *gh.Client) (*gh.Workflow, error) {
opts := &gh.ListOptions{PerPage: PerPageMax}
for {
wfs, res, err := client.Actions.ListWorkflows(ctx, r.Owner, r.Repo, opts)
if err != nil {
return nil, err
}
for _, wf := range wfs.Workflows {
if wf.GetName() == r.WorkflowName {
return wf, nil
}
}
if res.NextPage == 0 {
return nil, fmt.Errorf("no workflow matching %s could be found", r.WorkflowName)
}
opts.Page = res.NextPage
}
}
// getWorkflowRuns gets teh workflow runs associated with a workflow ID.
func (r *ListWorkflowRunsReq) getWorkflowRuns(ctx context.Context, client *gh.Client, id int64) ([]*WorkflowRun, error) {
var runs []*WorkflowRun
opts := &gh.ListWorkflowRunsOptions{
Actor: r.Actor,
Branch: r.Branch,
@ -182,22 +158,7 @@ func (r *ListWorkflowRunsReq) getWorkflowRuns(ctx context.Context, client *gh.Cl
opts.CheckSuiteID = r.CheckSuiteID
}
for {
wfrs, res, err := client.Actions.ListWorkflowRunsByID(ctx, r.Owner, r.Repo, id, opts)
if err != nil {
return nil, err
}
for _, r := range wfrs.WorkflowRuns {
runs = append(runs, &WorkflowRun{Run: r})
}
if res.NextPage == 0 {
return runs, nil
}
opts.ListOptions.Page = res.NextPage
}
return getWorkflowRuns(ctx, client, r.Owner, r.Repo, id, opts)
}
// getWorkflowCheckRuns gets the check suite runs associated with the workflow runs.

View File

@ -8,7 +8,7 @@ import (
"fmt"
"log/slog"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
slogctx "github.com/veqryn/slog-context"
)

View File

@ -12,7 +12,7 @@ import (
"os"
"path/filepath"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
libgit "github.com/hashicorp/vault/tools/pipeline/internal/pkg/git"
"github.com/jedib0t/go-pretty/v6/table"
slogctx "github.com/veqryn/slog-context"

View File

@ -8,7 +8,7 @@ import (
"io"
"testing"
libgithub "github.com/google/go-github/v68/github"
libgithub "github.com/google/go-github/v74/github"
"github.com/stretchr/testify/require"
)

View File

@ -0,0 +1,119 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package github
import (
"context"
"fmt"
"log/slog"
gh "github.com/google/go-github/v74/github"
slogctx "github.com/veqryn/slog-context"
)
// getWorkflow attempts to locate the workflow associated with our workflow name.
func getWorkflow(
ctx context.Context,
client *gh.Client,
owner string,
repo string,
name string,
) (*gh.Workflow, error) {
slog.Default().DebugContext(slogctx.Append(ctx,
slog.String("owner", owner),
slog.String("repo", repo),
slog.String("name", name),
), "getting github actions workflow")
opts := &gh.ListOptions{PerPage: PerPageMax}
for {
wfs, res, err := client.Actions.ListWorkflows(ctx, owner, repo, opts)
if err != nil {
return nil, err
}
for _, wf := range wfs.Workflows {
if wf.GetName() == name {
return wf, nil
}
}
if res.NextPage == 0 {
return nil, fmt.Errorf("no workflow matching %s could be found", name)
}
opts.Page = res.NextPage
}
}
// getWorkflowRuns gets the workflow runs associated with a workflow ID.
func getWorkflowRuns(
ctx context.Context,
client *gh.Client,
owner string,
repo string,
id int64,
opts *gh.ListWorkflowRunsOptions,
) ([]*WorkflowRun, error) {
slog.Default().DebugContext(slogctx.Append(ctx,
slog.String("owner", owner),
slog.String("repo", repo),
slog.Int64("id", id),
), "getting github actions workflow runs")
var runs []*WorkflowRun
opts.ListOptions = gh.ListOptions{PerPage: PerPageMax}
for {
wfrs, res, err := client.Actions.ListWorkflowRunsByID(ctx, owner, repo, id, opts)
if err != nil {
return nil, err
}
for _, r := range wfrs.WorkflowRuns {
runs = append(runs, &WorkflowRun{Run: r})
}
if res.NextPage == 0 {
return runs, nil
}
opts.ListOptions.Page = res.NextPage
}
}
// getWorkflowRunArtifacts gets the artifacts associated with a workflow run
func getWorkflowRunArtifacts(
ctx context.Context,
client *gh.Client,
owner string,
repo string,
id int64,
) (gh.ArtifactList, error) {
slog.Default().DebugContext(slogctx.Append(ctx,
slog.String("owner", owner),
slog.String("repo", repo),
slog.Int64("id", id),
), "getting github actions workflow run artifacts")
opts := &gh.ListOptions{PerPage: PerPageMax}
artifacts := gh.ArtifactList{}
for {
arts, res, err := client.Actions.ListWorkflowRunArtifacts(ctx, owner, repo, id, opts)
if err != nil {
return artifacts, err
}
newTotal := artifacts.GetTotalCount() + arts.GetTotalCount()
artifacts.TotalCount = &newTotal
artifacts.Artifacts = append(artifacts.Artifacts, arts.Artifacts...)
if res.NextPage == 0 {
return artifacts, nil
}
opts.Page = res.NextPage
}
}

View File

@ -0,0 +1,140 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcp
import (
"context"
"log/slog"
"net/http"
"os"
"sync"
slogctx "github.com/veqryn/slog-context"
)
// Client is the HCP client.
type Client struct {
Environment Environment
HTTPClient *http.Client
// Basic auth
Username string
Password string
once *sync.Once
}
// ClientOpt is an option to NewClient.
type ClientOpt func(*Client)
// Requester is an interface that defines a request that can be configured
// for an environment.
type Requester interface {
Request(Environment) (*http.Request, error)
}
// Environment is an HCP portal environment
type Environment string
const (
EnvironmentUnknown Environment = ""
EnvironmentDev Environment = "dev"
EnvironmentInt Environment = "int"
EnvironmentProd Environment = "prod"
)
// Addr is the URL for each environment
func (g Environment) Addr() string {
switch g {
case EnvironmentDev:
return "https://api.hcp.dev"
case EnvironmentInt:
return "https://api.hcp.to"
case EnvironmentProd:
return "https://api.hashicorp.cloud"
default:
return ""
}
}
// NewClient takes none-or-more options and returns a new Client.
func NewClient(opts ...ClientOpt) *Client {
c := &Client{
HTTPClient: http.DefaultClient,
}
for _, opt := range opts {
opt(c)
}
return c
}
// WithEnvironment sets the client environment.
func WithEnvironment(env Environment) ClientOpt {
return func(c *Client) {
c.Environment = env
}
}
// WithHTTPClient sets the client HTTP Client.
func WithHTTPClient(httpClient *http.Client) ClientOpt {
return func(c *Client) {
c.HTTPClient = httpClient
}
}
// WithUsername sets the basic auth username for internal APIs.
func WithUsername(username string) ClientOpt {
return func(c *Client) {
c.Username = username
}
}
// WithUsername sets the base auth password for internal APIs>
func WithPassword(password string) ClientOpt {
return func(c *Client) {
c.Password = password
}
}
// WithLoadTokenFromEnv sets the basic auth username and token from known env
// vars.
func WithLoadAuthFromEnv() ClientOpt {
return func(client *Client) {
if username, ok := os.LookupEnv("HCP_USERNAME"); ok {
client.Username = username
}
if password, ok := os.LookupEnv("HCP_PASSWORD"); ok {
client.Password = password
}
}
}
// Do takes in a Requester and performs the request. It returns the raw http
// Response.
func (c *Client) Do(ctx context.Context, req Requester) (*http.Response, error) {
logArgs := []any{
slog.String("env", string(c.Environment)),
slog.String("api-addr", string(c.Environment.Addr())),
}
httpReq, err := req.Request(c.Environment)
if err != nil {
slog.Default().ErrorContext(slogctx.Append(ctx,
append(logArgs, slog.String("error", err.Error()))),
"performing request",
)
return nil, err
}
logArgs = append(logArgs,
slog.String("method", httpReq.Method),
slog.String("url", httpReq.URL.String()),
)
ctx = slogctx.Append(ctx, logArgs...)
slog.Default().DebugContext(ctx, "performing request")
httpReq.SetBasicAuth(c.Username, c.Password)
httpReq.WithContext(ctx)
return c.HTTPClient.Do(httpReq)
}

View File

@ -0,0 +1,221 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcp
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"net/url"
"time"
"github.com/jedib0t/go-pretty/v6/table"
slogctx "github.com/veqryn/slog-context"
)
// GetLatestProductVersionReq is an HCP image service request to get the latest
// product version. It can also be used to get information for other images
// when configured with different constraints.
type GetLatestProductVersionReq struct {
ProductName string
ProductVersionConstraint string
HostManagerVersionConstraint string
CloudProvider string
CloudRegion string
Availability GetLatestProductVersionAvailability
ExcludeReleaseCandidates bool
}
// GetLatestProductVersionAvailability describes the availability state of an
// image.
type GetLatestProductVersionAvailability string
// GetLatestProductVersionRes is a response from a request to get the latest
// image from the HCP image service.
type GetLatestProductVersionRes struct {
Response *http.Response
Image *HCPImage `json:"image,omitempty"`
}
// HCPRegion is a cloud region for the image.
type HCPRegion struct {
Provider string `json:"provider,omitempty"`
Region string `json:"region,omitempty"`
}
// HCPImageReference is the image reference information.
type HCPImageReference struct {
ImageID string `json:"image_id,omitempty"`
Region *HCPRegion `json:"region,omitempty"`
}
// HCPImage is an image in the HCP image service.
type HCPImage struct {
ID string `json:"id,omitempty"`
ProductName string `json:"product_name,omitempty"`
ProductVersion string `json:"product_version,omitempty"`
HostManagerVersion string `json:"host_manager_version,omitempty"`
Region *HCPRegion `json:"region,omitempty"`
Availability string `json:"availability,omitempty"`
AWS *HCPImageReference `json:"aws,omitempty"`
Azure *HCPImageReference `json:"azure,omitempty"`
OSVersion string `json:"os_version,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
const (
GetLatestProductVersionAvailabilityUnknown GetLatestProductVersionAvailability = ""
GetLatestProductVersionAvailabilityDisabled GetLatestProductVersionAvailability = "disabled"
GetLatestProductVersionAvailabilityInternal GetLatestProductVersionAvailability = "internal"
GetLatestProductVersionAvailabilityPublic GetLatestProductVersionAvailability = "public"
GetLatestProductVersionAvailabilityBeta GetLatestProductVersionAvailability = "beta"
)
// ID returns the availability into the corresponding integer enum.
func (g GetLatestProductVersionAvailability) ID() string {
switch g {
case GetLatestProductVersionAvailabilityDisabled:
return "1"
case GetLatestProductVersionAvailabilityInternal:
return "2"
case GetLatestProductVersionAvailabilityPublic:
return "3"
case GetLatestProductVersionAvailabilityBeta:
return "4"
default:
return "0"
}
}
const imageServicePath = "image/2009-12-19/.internal/latestproductversion"
// Request takes an environment and produces an HTTP request that the client
// can execute.
func (r *GetLatestProductVersionReq) Request(env Environment) (*http.Request, error) {
reqURL, err := url.JoinPath(env.Addr(), imageServicePath)
if err != nil {
return nil, err
}
req, err := http.NewRequest(http.MethodGet, reqURL, nil)
if err != nil {
return nil, err
}
query := req.URL.Query()
if r.ProductName != "" {
query.Add("product_name", r.ProductName)
}
if r.ProductVersionConstraint != "" {
query.Add("product_version_constraint", r.ProductVersionConstraint)
}
if r.HostManagerVersionConstraint != "" {
query.Add("host_manager_version_constraint", r.HostManagerVersionConstraint)
}
if r.CloudProvider != "" {
query.Add("region.provider", r.CloudProvider)
}
if r.CloudRegion != "" {
query.Add("region.region", r.CloudRegion)
}
if r.Availability != "" {
query.Add("availability", r.Availability.ID())
}
if r.ExcludeReleaseCandidates {
query.Add("exclude_release_candidates", fmt.Sprintf("%t", r.ExcludeReleaseCandidates))
}
req.URL.RawQuery = query.Encode()
return req, nil
}
// Run runs the request to find an HCP image that matches the request criteria.
func (r *GetLatestProductVersionReq) Run(ctx context.Context, client *Client) (*GetLatestProductVersionRes, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
res := &GetLatestProductVersionRes{}
ctx = slogctx.Append(ctx,
slog.String("availability", string(r.Availability)),
slog.String("availability-id", r.Availability.ID()),
slog.String("cloud", r.CloudProvider),
slog.Bool("exclude-release-candidates", r.ExcludeReleaseCandidates),
slog.String("host-manager-version-constraint", r.HostManagerVersionConstraint),
slog.String("product", r.ProductName),
slog.String("product-version-constraint", r.ProductVersionConstraint),
slog.String("region", r.CloudRegion),
)
slog.Default().DebugContext(ctx, "getting latest HCP product version")
var err error
res.Response, err = client.Do(ctx, r)
if err != nil {
return res, err
}
defer res.Response.Body.Close()
bytes, err := io.ReadAll(res.Response.Body)
if err != nil {
return res, err
}
if err = json.Unmarshal(bytes, res); err != nil {
return res, err
}
if res.Response.StatusCode > 299 {
return res, fmt.Errorf("received unexpected http response code: %d", res.Response.StatusCode)
}
return res, nil
}
// ToJSON marshals the response to JSON.
func (r *GetLatestProductVersionRes) ToJSON() ([]byte, error) {
b, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("marshaling latest HCP image response to JSON: %w", err)
}
return b, nil
}
// ToTable marshals the response to a text table.
func (r *GetLatestProductVersionRes) ToTable() table.Writer {
t := table.NewWriter()
t.Style().Options.DrawBorder = false
t.Style().Options.SeparateColumns = false
t.Style().Options.SeparateFooter = false
t.Style().Options.SeparateHeader = false
t.Style().Options.SeparateRows = false
t.AppendHeader(table.Row{"name", "id", "cloud", "region", "version", "created_at"})
var imageID string
if aws := r.Image.AWS; aws != nil {
imageID = aws.ImageID
}
if azure := r.Image.Azure; azure != nil {
imageID = azure.ImageID
}
t.AppendRow(table.Row{
r.Image.ProductName,
imageID,
r.Image.Region.Provider,
r.Image.Region.Region,
r.Image.ProductVersion,
r.Image.CreatedAt.String(),
})
return t
}

View File

@ -0,0 +1,83 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcp
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func Test_GetLatestProductVersionReq_Request(t *testing.T) {
t.Parallel()
for name, test := range map[string]struct {
req *GetLatestProductVersionReq
expectedURL string
}{
"no query args": {
&GetLatestProductVersionReq{},
"https://api.hcp.dev/image/2009-12-19/.internal/latestproductversion",
},
"all query args": {
&GetLatestProductVersionReq{
Availability: GetLatestProductVersionAvailabilityPublic,
CloudRegion: "us-east-1",
CloudProvider: "aws",
ExcludeReleaseCandidates: true,
ProductName: "vault",
ProductVersionConstraint: "1.21.0-beta1+ent-2cf0b2f",
},
"https://api.hcp.dev/image/2009-12-19/.internal/latestproductversion?availability=3&exclude_release_candidates=true&product_name=vault&product_version_constraint=1.21.0-beta1%2Bent-2cf0b2f&region.provider=aws&region.region=us-east-1",
},
} {
t.Run(name, func(t *testing.T) {
t.Parallel()
req, err := test.req.Request(EnvironmentDev)
require.NoError(t, err)
require.Equal(t, test.expectedURL, req.URL.String())
})
}
}
const hcpImageJSON = `{"image":{"id":"c613a76a-7c7f-484d-b21f-f99603dacee7","product_name":"vault","product_version":"v1.21.0-beta1+ent-bf26069","host_manager_version":"0.2.1806001022+1f8c65e9","region":{"provider":"aws","region":"us-west-2"},"availability":"PUBLIC","aws":{"image_id":"ami-037fc9428b5fc8d6a","region":{"provider":"aws","region":"us-west-2"}},"os_version":"","created_at":"2025-07-23T06:45:04.029Z","updated_at":"2025-07-23T06:45:04.008Z"}}`
func Test_GetLatestProductVersionRes_Unmarshal(t *testing.T) {
t.Parallel()
createdAt, err := time.Parse(time.RFC3339, "2025-07-23T06:45:04.029Z")
require.NoError(t, err)
updatedAt, err := time.Parse(time.RFC3339, "2025-07-23T06:45:04.008Z")
require.NoError(t, err)
res := &GetLatestProductVersionRes{}
require.NoError(t, json.Unmarshal([]byte(hcpImageJSON), res))
expect := &GetLatestProductVersionRes{
Image: &HCPImage{
ID: "c613a76a-7c7f-484d-b21f-f99603dacee7",
ProductName: "vault",
ProductVersion: "v1.21.0-beta1+ent-bf26069",
HostManagerVersion: "0.2.1806001022+1f8c65e9",
Region: &HCPRegion{
Provider: "aws",
Region: "us-west-2",
},
Availability: "PUBLIC",
AWS: &HCPImageReference{
ImageID: "ami-037fc9428b5fc8d6a",
Region: &HCPRegion{
Provider: "aws",
Region: "us-west-2",
},
},
OSVersion: "",
CreatedAt: createdAt,
UpdatedAt: updatedAt,
},
}
require.EqualValues(t, expect, res)
}

View File

@ -0,0 +1,73 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package hcp
import (
"context"
"log/slog"
"time"
"github.com/avast/retry-go/v4"
slogctx "github.com/veqryn/slog-context"
)
// WaitForImageReq is a request to wait for an image to be available in the
// image service.
type WaitForImageReq struct {
Req *GetLatestProductVersionReq `json:"req,omitempty"`
Delay time.Duration `json:"delay,omitempty"`
}
// WaitForImageRes is a response to a WaitForImageReq.
type WaitForImageRes struct {
Res *GetLatestProductVersionRes `json:"res,omitempty"`
}
// Run runs the wait for image request.
func (r *WaitForImageReq) Run(ctx context.Context, client *Client) (*WaitForImageRes, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
slog.Default().DebugContext(ctx, "waiting for HCP image to be available")
res := &WaitForImageRes{
Res: &GetLatestProductVersionRes{},
}
attempt := 0
err := retry.Do(
func() error {
attempt++
// Limit each request to the image service to 5 seconds max
reqCtx, reqCancel := context.WithTimeout(ctx, 5*time.Second)
defer reqCancel()
reqRes, err := r.Req.Run(reqCtx, client)
if reqRes != nil {
if reqRes.Response != nil {
res.Res.Response = reqRes.Response
}
if reqRes.Image != nil {
res.Res.Image = reqRes.Image
}
}
if err != nil {
slog.Default().DebugContext(
slogctx.Append(ctx,
slog.Int("attempt", attempt),
slog.String("error", err.Error())),
"attempt to get HCP image details failed",
)
}
return err
},
retry.Context(ctx),
retry.Delay(r.Delay),
)
return res, err
}