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

Commit 400173f

Browse files
Merge pull request #4251 from paladin-devops/f-hcp-packer-config-sourcer
HCP Packer Config Sourcer plugin
2 parents 4905b60 + 1653b30 commit 400173f

21 files changed

+810
-15
lines changed

.changelog/4251.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
```release-note:feature
2+
plugin/packer: A "packer" config sourcer plugin to source machine image IDs from
3+
an HCP Packer channel.
4+
```

builtin/packer/config_sourcer.go

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
package packer
2+
3+
import (
4+
"context"
5+
6+
"github.com/hashicorp/go-hclog"
7+
packer "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2021-04-30/client/packer_service"
8+
hcpconfig "github.com/hashicorp/hcp-sdk-go/config"
9+
"github.com/hashicorp/hcp-sdk-go/httpclient"
10+
"github.com/mitchellh/mapstructure"
11+
"google.golang.org/grpc/codes"
12+
"google.golang.org/grpc/status"
13+
14+
"github.com/hashicorp/waypoint-plugin-sdk/component"
15+
"github.com/hashicorp/waypoint-plugin-sdk/docs"
16+
pb "github.com/hashicorp/waypoint-plugin-sdk/proto/gen"
17+
)
18+
19+
type ConfigSourcer struct {
20+
config sourceConfig
21+
client packer.Client
22+
}
23+
24+
type sourceConfig struct {
25+
// The HCP Client ID to authenticate to HCP
26+
ClientId string `hcl:"client_id,optional"`
27+
28+
// The HCP Client Secret to authenticate to HCP
29+
ClientSecret string `hcl:"client_secret,optional"`
30+
31+
// The HCP Organization ID to authenticate to in HCP
32+
OrganizationId string `hcl:"organization_id,attr"`
33+
34+
// The HCP Project ID within the organization to authenticate to in HCP
35+
ProjectId string `hcl:"project_id,attr"`
36+
}
37+
38+
type reqConfig struct {
39+
// The name of the HCP Packer registry bucket from which to source an image
40+
Bucket string `hcl:"bucket,attr"`
41+
42+
// The name of the HCP Packer registry bucket channel from which to source an image
43+
Channel string `hcl:"channel,attr"`
44+
45+
// The region of the machine image to be pulled
46+
Region string `hcl:"region,attr"`
47+
48+
// The cloud provider of the machine image to be pulled
49+
Cloud string `hcl:"cloud,attr"`
50+
}
51+
52+
// Config implements component.Configurable
53+
func (cs *ConfigSourcer) Config() (interface{}, error) {
54+
return &cs.config, nil
55+
}
56+
57+
// ReadFunc implements component.ConfigSourcer
58+
func (cs *ConfigSourcer) ReadFunc() interface{} {
59+
return cs.read
60+
}
61+
62+
// StopFunc implements component.ConfigSourcer
63+
func (cs *ConfigSourcer) StopFunc() interface{} {
64+
return cs.stop
65+
}
66+
67+
func (cs *ConfigSourcer) read(
68+
ctx context.Context,
69+
log hclog.Logger,
70+
reqs []*component.ConfigRequest,
71+
) ([]*pb.ConfigSource_Value, error) {
72+
73+
// If the user has explicitly set the client ID and secret for the config
74+
// sourcer, we use that. Otherwise, we use environment variables.
75+
opts := hcpconfig.FromEnv()
76+
if cs.config.ClientId != "" && cs.config.ClientSecret != "" {
77+
opts = hcpconfig.WithClientCredentials(cs.config.ClientId, cs.config.ClientSecret)
78+
}
79+
hcpConfig, err := hcpconfig.NewHCPConfig(opts)
80+
if err != nil {
81+
return nil, err
82+
}
83+
84+
hcpClient, err := httpclient.New(httpclient.Config{
85+
HCPConfig: hcpConfig,
86+
})
87+
if err != nil {
88+
return nil, err
89+
}
90+
91+
hcpPackerClient := packer.New(hcpClient, nil)
92+
channelParams := packer.NewPackerServiceGetChannelParams()
93+
channelParams.LocationOrganizationID = cs.config.OrganizationId
94+
channelParams.LocationProjectID = cs.config.ProjectId
95+
96+
var results []*pb.ConfigSource_Value
97+
for _, req := range reqs {
98+
result := &pb.ConfigSource_Value{Name: req.Name}
99+
results = append(results, result)
100+
101+
var packerConfig reqConfig
102+
// We serialize the config sourcer settings to the reqConfig struct.
103+
if err = mapstructure.WeakDecode(req.Config, &packerConfig); err != nil {
104+
result.Result = &pb.ConfigSource_Value_Error{
105+
Error: status.New(codes.Aborted, err.Error()).Proto(),
106+
}
107+
return nil, err
108+
}
109+
channelParams.BucketSlug = packerConfig.Bucket
110+
channelParams.Slug = packerConfig.Channel
111+
112+
// An HCP Packer channel points to a single iteration of a bucket.
113+
channel, err := hcpPackerClient.PackerServiceGetChannel(channelParams, nil)
114+
if err != nil {
115+
return nil, err
116+
}
117+
log.Debug("retrieved HCP Packer channel", "channel", channel.Payload.Channel.Slug)
118+
iteration := channel.Payload.Channel.Iteration
119+
120+
// An iteration can have multiple builds, so we check for the first build
121+
// with the matching cloud provider and region.
122+
for _, build := range iteration.Builds {
123+
if build.CloudProvider == packerConfig.Cloud {
124+
log.Debug("found build with matching cloud provider",
125+
"cloud provider", build.CloudProvider,
126+
"build ID", build.ID)
127+
for _, image := range build.Images {
128+
if image.Region == packerConfig.Region {
129+
log.Debug("found image with matching region",
130+
"region", image.Region,
131+
"image ID", image.ID)
132+
result.Result = &pb.ConfigSource_Value_Value{
133+
// The ImageID is the Cloud Image ID or URL string
134+
// identifying this image for the builder that built it,
135+
// so this is returned to Waypoint.
136+
Value: image.ImageID,
137+
}
138+
}
139+
}
140+
}
141+
}
142+
}
143+
144+
return results, nil
145+
}
146+
147+
func (cs *ConfigSourcer) stop() error {
148+
return nil
149+
}
150+
151+
func (cs *ConfigSourcer) Documentation() (*docs.Documentation, error) {
152+
doc, err := docs.New(
153+
docs.FromConfig(&sourceConfig{}),
154+
docs.RequestFromStruct(&reqConfig{}),
155+
)
156+
if err != nil {
157+
return nil, err
158+
}
159+
160+
doc.Description("Retrieve the image ID of an image whose metadata is pushed " +
161+
"to an HCP Packer registry. The image ID is that of the HCP Packer bucket" +
162+
"iteration assigned to the configured channel, with a matching cloud provider" +
163+
"and region.")
164+
165+
doc.Example(`
166+
// The waypoint.hcl file
167+
project = "example-reactjs-project"
168+
169+
variable "image" {
170+
default = dynamic("packer", {
171+
bucket = "nginx"
172+
channel = "base"
173+
region = "docker"
174+
cloud_provider = "docker"
175+
}
176+
type = string
177+
description = "The name of the base image to use for building app Docker images."
178+
}
179+
180+
app "example-reactjs" {
181+
build {
182+
use "docker" {
183+
dockerfile = templatefile("${path.app}"/Dockerfile, {
184+
base_image = var.image
185+
}
186+
}
187+
188+
deploy {
189+
use "docker" {}
190+
}
191+
}
192+
193+
194+
# Multi-stage Dockerfile example
195+
FROM node:19.2-alpine as build
196+
WORKDIR /app
197+
ENV PATH /app/node_modules/.bin:$PATH
198+
COPY package.json ./
199+
COPY package-lock.json ./
200+
RUN npm ci --silent
201+
RUN npm install [email protected] -g --silent
202+
COPY . ./
203+
RUN npm run build
204+
205+
# ${base_image} below is the Docker repository and tag, templated to the Dockerfile
206+
FROM ${base_image}
207+
COPY nginx/default.conf /etc/nginx/conf.d/
208+
COPY --from=build /app/build /usr/share/nginx/html
209+
EXPOSE 80
210+
CMD ["nginx", "-g", "daemon off;"]
211+
`)
212+
213+
doc.SetRequestField(
214+
"bucket",
215+
"The name of the HCP Packer bucket from which to source an image.",
216+
)
217+
218+
doc.SetRequestField(
219+
"channel",
220+
"The name of the HCP Packer channel from which to source the latest image.",
221+
)
222+
223+
doc.SetRequestField(
224+
"region",
225+
"The region set for the machine image's cloud provider.",
226+
)
227+
228+
doc.SetRequestField(
229+
"cloud",
230+
"The cloud provider of the machine image to source",
231+
)
232+
233+
doc.SetField(
234+
"organization_id",
235+
"The HCP organization ID.",
236+
)
237+
238+
doc.SetField(
239+
"project_id",
240+
"The HCP Project ID.",
241+
)
242+
243+
doc.SetField(
244+
"client_id",
245+
"The OAuth2 Client ID for HCP API operations.",
246+
docs.EnvVar("HCP_CLIENT_ID"),
247+
)
248+
249+
doc.SetField(
250+
"client_secret",
251+
"The OAuth2 Client Secret for HCP API operations.",
252+
docs.EnvVar("HCP_CLIENT_SECRET"),
253+
)
254+
255+
return doc, nil
256+
}

builtin/packer/packer.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package packer
2+
3+
import (
4+
sdk "github.com/hashicorp/waypoint-plugin-sdk"
5+
)
6+
7+
var Options = []sdk.Option{
8+
sdk.WithComponents(&ConfigSourcer{}),
9+
}

builtin/tfc/config_sourcer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func (cs *ConfigSourcer) read(
192192
L := log.With("workspace", tfcReq.Workspace, "organization", tfcReq.Organization)
193193

194194
// We have to map the organization + workspace to a workspace-id, so we do that first.
195-
// the workspaceIds map is never cleared beacuse the configuration about which
195+
// the workspaceIds map is never cleared because the configuration about which
196196
// organization + workspace that is in use is static in the context of a config
197197
// sourcer.
198198
key := tfcReq.Organization + "/" + tfcReq.Workspace

embedJson/gen/builder-packer.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"mappers": null,
3+
"name": "packer",
4+
"optionalFields": null,
5+
"requiredFields": null,
6+
"type": "builder",
7+
"use": "the [`use` stanza](/docs/waypoint-hcl/use) for this plugin."
8+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"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.",
3+
"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;\"]",
4+
"mappers": null,
5+
"name": "packer",
6+
"optionalFields": null,
7+
"optionalSourceFields": [
8+
{
9+
"Field": "client_id",
10+
"Type": "string",
11+
"Synopsis": "The OAuth2 Client ID for HCP API operations.",
12+
"Summary": "",
13+
"Optional": true,
14+
"Default": "",
15+
"EnvVar": "HCP_CLIENT_ID",
16+
"Category": false,
17+
"SubFields": null
18+
},
19+
{
20+
"Field": "client_secret",
21+
"Type": "string",
22+
"Synopsis": "The OAuth2 Client Secret for HCP API operations.",
23+
"Summary": "",
24+
"Optional": true,
25+
"Default": "",
26+
"EnvVar": "HCP_CLIENT_SECRET",
27+
"Category": false,
28+
"SubFields": null
29+
}
30+
],
31+
"requiredFields": [
32+
{
33+
"Field": "bucket",
34+
"Type": "string",
35+
"Synopsis": "The name of the HCP Packer bucket from which to source an image.",
36+
"Summary": "",
37+
"Optional": false,
38+
"Default": "",
39+
"EnvVar": "",
40+
"Category": false,
41+
"SubFields": null
42+
},
43+
{
44+
"Field": "channel",
45+
"Type": "string",
46+
"Synopsis": "The name of the HCP Packer channel from which to source the latest image.",
47+
"Summary": "",
48+
"Optional": false,
49+
"Default": "",
50+
"EnvVar": "",
51+
"Category": false,
52+
"SubFields": null
53+
},
54+
{
55+
"Field": "cloud",
56+
"Type": "string",
57+
"Synopsis": "The cloud provider of the machine image to source",
58+
"Summary": "",
59+
"Optional": false,
60+
"Default": "",
61+
"EnvVar": "",
62+
"Category": false,
63+
"SubFields": null
64+
},
65+
{
66+
"Field": "region",
67+
"Type": "string",
68+
"Synopsis": "The region set for the machine image's cloud provider.",
69+
"Summary": "",
70+
"Optional": false,
71+
"Default": "",
72+
"EnvVar": "",
73+
"Category": false,
74+
"SubFields": null
75+
}
76+
],
77+
"requiredSourceFields": [
78+
{
79+
"Field": "organization_id",
80+
"Type": "string",
81+
"Synopsis": "The HCP organization ID.",
82+
"Summary": "",
83+
"Optional": false,
84+
"Default": "",
85+
"EnvVar": "",
86+
"Category": false,
87+
"SubFields": null
88+
},
89+
{
90+
"Field": "project_id",
91+
"Type": "string",
92+
"Synopsis": "The HCP Project ID.",
93+
"Summary": "",
94+
"Optional": false,
95+
"Default": "",
96+
"EnvVar": "",
97+
"Category": false,
98+
"SubFields": null
99+
}
100+
],
101+
"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",
102+
"type": "configsourcer",
103+
"use": "`dynamic` for sourcing [configuration values](/docs/app-config/dynamic) or [input variable values](/docs/waypoint-hcl/variables/dynamic)."
104+
}

0 commit comments

Comments
 (0)