Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

HCP Packer Config Sourcer plugin #4251

Merged
merged 9 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .changelog/4251.txt
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.
```
256 changes: 256 additions & 0 deletions builtin/packer/config_sourcer.go
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"`
}
paladin-devops marked this conversation as resolved.
Show resolved Hide resolved

// 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.
Copy link
Member

Choose a reason for hiding this comment

The 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 latest build?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With one packer build operation, I have never tried to push more than one image of the same cloud provider and region to the same bucket in an HCP Packer registry. 🤔 I don't know for certain if it's possible or not, but maybe we could add more specific input parameters here in a follow up PR if it seems useful?

Copy link
Member

Choose a reason for hiding this comment

The 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
}
9 changes: 9 additions & 0 deletions builtin/packer/packer.go
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{}),
}
2 changes: 1 addition & 1 deletion builtin/tfc/config_sourcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ func (cs *ConfigSourcer) read(
L := log.With("workspace", tfcReq.Workspace, "organization", tfcReq.Organization)

// We have to map the organization + workspace to a workspace-id, so we do that first.
// the workspaceIds map is never cleared beacuse the configuration about which
// the workspaceIds map is never cleared because the configuration about which
// organization + workspace that is in use is static in the context of a config
// sourcer.
key := tfcReq.Organization + "/" + tfcReq.Workspace
Expand Down
8 changes: 8 additions & 0 deletions embedJson/gen/builder-packer.json
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."
}
104 changes: 104 additions & 0 deletions embedJson/gen/configsourcer-packer.json
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)."
}
Loading