-
Notifications
You must be signed in to change notification settings - Fork 330
HCP Packer Config Sourcer plugin #4251
Changes from all commits
1f16dbe
5a40d72
d76798b
3c7f53d
f4905da
14e1173
333e1ad
110be2e
1653b30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| ```release-note:feature | ||
| plugin/packer: A "packer" config sourcer plugin to source machine image IDs from | ||
| an HCP Packer channel. | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| package packer | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/hashicorp/go-hclog" | ||
| packer "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2021-04-30/client/packer_service" | ||
| hcpconfig "github.com/hashicorp/hcp-sdk-go/config" | ||
| "github.com/hashicorp/hcp-sdk-go/httpclient" | ||
| "github.com/mitchellh/mapstructure" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/status" | ||
|
|
||
| "github.com/hashicorp/waypoint-plugin-sdk/component" | ||
| "github.com/hashicorp/waypoint-plugin-sdk/docs" | ||
| pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen" | ||
| ) | ||
|
|
||
| type ConfigSourcer struct { | ||
| config sourceConfig | ||
| client packer.Client | ||
| } | ||
|
|
||
| type sourceConfig struct { | ||
| // The HCP Client ID to authenticate to HCP | ||
| ClientId string `hcl:"client_id,optional"` | ||
|
|
||
| // The HCP Client Secret to authenticate to HCP | ||
| ClientSecret string `hcl:"client_secret,optional"` | ||
|
|
||
| // The HCP Organization ID to authenticate to in HCP | ||
| OrganizationId string `hcl:"organization_id,attr"` | ||
|
|
||
| // The HCP Project ID within the organization to authenticate to in HCP | ||
| ProjectId string `hcl:"project_id,attr"` | ||
| } | ||
|
|
||
| type reqConfig struct { | ||
| // The name of the HCP Packer registry bucket from which to source an image | ||
| Bucket string `hcl:"bucket,attr"` | ||
|
|
||
| // The name of the HCP Packer registry bucket channel from which to source an image | ||
| Channel string `hcl:"channel,attr"` | ||
|
|
||
| // The region of the machine image to be pulled | ||
| Region string `hcl:"region,attr"` | ||
|
|
||
| // The cloud provider of the machine image to be pulled | ||
| Cloud string `hcl:"cloud,attr"` | ||
| } | ||
|
|
||
| // Config implements component.Configurable | ||
| func (cs *ConfigSourcer) Config() (interface{}, error) { | ||
| return &cs.config, nil | ||
| } | ||
|
|
||
| // ReadFunc implements component.ConfigSourcer | ||
| func (cs *ConfigSourcer) ReadFunc() interface{} { | ||
| return cs.read | ||
| } | ||
|
|
||
| // StopFunc implements component.ConfigSourcer | ||
| func (cs *ConfigSourcer) StopFunc() interface{} { | ||
| return cs.stop | ||
| } | ||
|
|
||
| func (cs *ConfigSourcer) read( | ||
| ctx context.Context, | ||
| log hclog.Logger, | ||
| reqs []*component.ConfigRequest, | ||
| ) ([]*pb.ConfigSource_Value, error) { | ||
|
|
||
| // If the user has explicitly set the client ID and secret for the config | ||
| // sourcer, we use that. Otherwise, we use environment variables. | ||
| opts := hcpconfig.FromEnv() | ||
| if cs.config.ClientId != "" && cs.config.ClientSecret != "" { | ||
| opts = hcpconfig.WithClientCredentials(cs.config.ClientId, cs.config.ClientSecret) | ||
| } | ||
| hcpConfig, err := hcpconfig.NewHCPConfig(opts) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| hcpClient, err := httpclient.New(httpclient.Config{ | ||
| HCPConfig: hcpConfig, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| hcpPackerClient := packer.New(hcpClient, nil) | ||
| channelParams := packer.NewPackerServiceGetChannelParams() | ||
| channelParams.LocationOrganizationID = cs.config.OrganizationId | ||
| channelParams.LocationProjectID = cs.config.ProjectId | ||
|
|
||
| var results []*pb.ConfigSource_Value | ||
| for _, req := range reqs { | ||
| result := &pb.ConfigSource_Value{Name: req.Name} | ||
| results = append(results, result) | ||
|
|
||
| var packerConfig reqConfig | ||
| // We serialize the config sourcer settings to the reqConfig struct. | ||
| if err = mapstructure.WeakDecode(req.Config, &packerConfig); err != nil { | ||
| result.Result = &pb.ConfigSource_Value_Error{ | ||
| Error: status.New(codes.Aborted, err.Error()).Proto(), | ||
| } | ||
| return nil, err | ||
| } | ||
| channelParams.BucketSlug = packerConfig.Bucket | ||
| channelParams.Slug = packerConfig.Channel | ||
|
|
||
| // An HCP Packer channel points to a single iteration of a bucket. | ||
| channel, err := hcpPackerClient.PackerServiceGetChannel(channelParams, nil) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| log.Debug("retrieved HCP Packer channel", "channel", channel.Payload.Channel.Slug) | ||
| iteration := channel.Payload.Channel.Iteration | ||
|
|
||
| // An iteration can have multiple builds, so we check for the first build | ||
| // with the matching cloud provider and region. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is interesting - is there a chance that we could unintentionally be picking the wrong build here? Or is "first" build essentially
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. With one
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure - totally fine to fix this up in a follow-up PR! Make an issue so we don't lose track 👍🏻 |
||
| for _, build := range iteration.Builds { | ||
| if build.CloudProvider == packerConfig.Cloud { | ||
| log.Debug("found build with matching cloud provider", | ||
| "cloud provider", build.CloudProvider, | ||
| "build ID", build.ID) | ||
| for _, image := range build.Images { | ||
| if image.Region == packerConfig.Region { | ||
| log.Debug("found image with matching region", | ||
| "region", image.Region, | ||
| "image ID", image.ID) | ||
| result.Result = &pb.ConfigSource_Value_Value{ | ||
| // The ImageID is the Cloud Image ID or URL string | ||
| // identifying this image for the builder that built it, | ||
| // so this is returned to Waypoint. | ||
| Value: image.ImageID, | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return results, nil | ||
| } | ||
|
|
||
| func (cs *ConfigSourcer) stop() error { | ||
| return nil | ||
| } | ||
|
|
||
| func (cs *ConfigSourcer) Documentation() (*docs.Documentation, error) { | ||
| doc, err := docs.New( | ||
| docs.FromConfig(&sourceConfig{}), | ||
| docs.RequestFromStruct(&reqConfig{}), | ||
| ) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| doc.Description("Retrieve the image ID of an image whose metadata is pushed " + | ||
| "to an HCP Packer registry. The image ID is that of the HCP Packer bucket" + | ||
| "iteration assigned to the configured channel, with a matching cloud provider" + | ||
| "and region.") | ||
|
|
||
| doc.Example(` | ||
| // The waypoint.hcl file | ||
| project = "example-reactjs-project" | ||
|
|
||
| variable "image" { | ||
| default = dynamic("packer", { | ||
| bucket = "nginx" | ||
| channel = "base" | ||
| region = "docker" | ||
| cloud_provider = "docker" | ||
| } | ||
| type = string | ||
| description = "The name of the base image to use for building app Docker images." | ||
| } | ||
|
|
||
| app "example-reactjs" { | ||
| build { | ||
| use "docker" { | ||
| dockerfile = templatefile("${path.app}"/Dockerfile, { | ||
| base_image = var.image | ||
| } | ||
| } | ||
|
|
||
| deploy { | ||
| use "docker" {} | ||
| } | ||
| } | ||
|
|
||
|
|
||
| # Multi-stage Dockerfile example | ||
| FROM node:19.2-alpine as build | ||
| WORKDIR /app | ||
| ENV PATH /app/node_modules/.bin:$PATH | ||
| COPY package.json ./ | ||
| COPY package-lock.json ./ | ||
| RUN npm ci --silent | ||
| RUN npm install [email protected] -g --silent | ||
| COPY . ./ | ||
| RUN npm run build | ||
|
|
||
| # ${base_image} below is the Docker repository and tag, templated to the Dockerfile | ||
| FROM ${base_image} | ||
| COPY nginx/default.conf /etc/nginx/conf.d/ | ||
| COPY --from=build /app/build /usr/share/nginx/html | ||
| EXPOSE 80 | ||
| CMD ["nginx", "-g", "daemon off;"] | ||
| `) | ||
|
|
||
| doc.SetRequestField( | ||
| "bucket", | ||
| "The name of the HCP Packer bucket from which to source an image.", | ||
| ) | ||
|
|
||
| doc.SetRequestField( | ||
| "channel", | ||
| "The name of the HCP Packer channel from which to source the latest image.", | ||
| ) | ||
|
|
||
| doc.SetRequestField( | ||
| "region", | ||
| "The region set for the machine image's cloud provider.", | ||
| ) | ||
|
|
||
| doc.SetRequestField( | ||
| "cloud", | ||
| "The cloud provider of the machine image to source", | ||
| ) | ||
|
|
||
| doc.SetField( | ||
| "organization_id", | ||
| "The HCP organization ID.", | ||
| ) | ||
|
|
||
| doc.SetField( | ||
| "project_id", | ||
| "The HCP Project ID.", | ||
| ) | ||
|
|
||
| doc.SetField( | ||
| "client_id", | ||
| "The OAuth2 Client ID for HCP API operations.", | ||
| docs.EnvVar("HCP_CLIENT_ID"), | ||
| ) | ||
|
|
||
| doc.SetField( | ||
| "client_secret", | ||
| "The OAuth2 Client Secret for HCP API operations.", | ||
| docs.EnvVar("HCP_CLIENT_SECRET"), | ||
| ) | ||
|
|
||
| return doc, nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package packer | ||
|
|
||
| import ( | ||
| sdk "github.com/hashicorp/waypoint-plugin-sdk" | ||
| ) | ||
|
|
||
| var Options = []sdk.Option{ | ||
| sdk.WithComponents(&ConfigSourcer{}), | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "mappers": null, | ||
| "name": "packer", | ||
| "optionalFields": null, | ||
| "requiredFields": null, | ||
| "type": "builder", | ||
| "use": "the [`use` stanza](/docs/waypoint-hcl/use) for this plugin." | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| { | ||
| "description": "Retrieve the image ID of an image whose metadata is pushed to an HCP Packer registry. The image ID is that of the HCP Packer bucketiteration assigned to the configured channel, with a matching cloud providerand region.", | ||
| "example": "// The waypoint.hcl file\nproject = \"example-reactjs-project\"\n\nvariable \"image\" {\n default = dynamic(\"packer\", {\n bucket = \"nginx\"\n channel = \"base\"\n region = \"docker\"\n cloud_provider = \"docker\"\n }\n type = string\n description = \"The name of the base image to use for building app Docker images.\"\n}\n\napp \"example-reactjs\" {\n build {\n use \"docker\" {\n dockerfile = templatefile(\"${path.app}\"/Dockerfile, {\n base_image = var.image\n }\n }\n\n deploy {\n use \"docker\" {}\n }\n}\n\n\n# Multi-stage Dockerfile example\nFROM node:19.2-alpine as build\nWORKDIR /app\nENV PATH /app/node_modules/.bin:$PATH\nCOPY package.json ./\nCOPY package-lock.json ./\nRUN npm ci --silent\nRUN npm install [email protected] -g --silent\nCOPY . ./\nRUN npm run build\n\n# ${base_image} below is the Docker repository and tag, templated to the Dockerfile\nFROM ${base_image}\nCOPY nginx/default.conf /etc/nginx/conf.d/\nCOPY --from=build /app/build /usr/share/nginx/html\nEXPOSE 80\nCMD [\"nginx\", \"-g\", \"daemon off;\"]", | ||
| "mappers": null, | ||
| "name": "packer", | ||
| "optionalFields": null, | ||
| "optionalSourceFields": [ | ||
| { | ||
| "Field": "client_id", | ||
| "Type": "string", | ||
| "Synopsis": "The OAuth2 Client ID for HCP API operations.", | ||
| "Summary": "", | ||
| "Optional": true, | ||
| "Default": "", | ||
| "EnvVar": "HCP_CLIENT_ID", | ||
| "Category": false, | ||
| "SubFields": null | ||
| }, | ||
| { | ||
| "Field": "client_secret", | ||
| "Type": "string", | ||
| "Synopsis": "The OAuth2 Client Secret for HCP API operations.", | ||
| "Summary": "", | ||
| "Optional": true, | ||
| "Default": "", | ||
| "EnvVar": "HCP_CLIENT_SECRET", | ||
| "Category": false, | ||
| "SubFields": null | ||
| } | ||
| ], | ||
| "requiredFields": [ | ||
| { | ||
| "Field": "bucket", | ||
| "Type": "string", | ||
| "Synopsis": "The name of the HCP Packer bucket from which to source an image.", | ||
| "Summary": "", | ||
| "Optional": false, | ||
| "Default": "", | ||
| "EnvVar": "", | ||
| "Category": false, | ||
| "SubFields": null | ||
| }, | ||
| { | ||
| "Field": "channel", | ||
| "Type": "string", | ||
| "Synopsis": "The name of the HCP Packer channel from which to source the latest image.", | ||
| "Summary": "", | ||
| "Optional": false, | ||
| "Default": "", | ||
| "EnvVar": "", | ||
| "Category": false, | ||
| "SubFields": null | ||
| }, | ||
| { | ||
| "Field": "cloud", | ||
| "Type": "string", | ||
| "Synopsis": "The cloud provider of the machine image to source", | ||
| "Summary": "", | ||
| "Optional": false, | ||
| "Default": "", | ||
| "EnvVar": "", | ||
| "Category": false, | ||
| "SubFields": null | ||
| }, | ||
| { | ||
| "Field": "region", | ||
| "Type": "string", | ||
| "Synopsis": "The region set for the machine image's cloud provider.", | ||
| "Summary": "", | ||
| "Optional": false, | ||
| "Default": "", | ||
| "EnvVar": "", | ||
| "Category": false, | ||
| "SubFields": null | ||
| } | ||
| ], | ||
| "requiredSourceFields": [ | ||
| { | ||
| "Field": "organization_id", | ||
| "Type": "string", | ||
| "Synopsis": "The HCP organization ID.", | ||
| "Summary": "", | ||
| "Optional": false, | ||
| "Default": "", | ||
| "EnvVar": "", | ||
| "Category": false, | ||
| "SubFields": null | ||
| }, | ||
| { | ||
| "Field": "project_id", | ||
| "Type": "string", | ||
| "Synopsis": "The HCP Project ID.", | ||
| "Summary": "", | ||
| "Optional": false, | ||
| "Default": "", | ||
| "EnvVar": "", | ||
| "Category": false, | ||
| "SubFields": null | ||
| } | ||
| ], | ||
| "sourceFieldsHelp": "Source Parameters\nThe parameters below are used with `waypoint config source-set` to configure\nthe behavior this plugin. These are _not_ used in `dynamic` calls. The\nparameters used for `dynamic` are in the previous section.\n", | ||
| "type": "configsourcer", | ||
| "use": "`dynamic` for sourcing [configuration values](/docs/app-config/dynamic) or [input variable values](/docs/waypoint-hcl/variables/dynamic)." | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.