Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement hcp_packer_registry block #11168

Merged
merged 21 commits into from
Jul 28, 2021
Merged
Show file tree
Hide file tree
Changes from 11 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
2 changes: 1 addition & 1 deletion command/core_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ func (c *CoreWrapper) PluginRequirements() (plugingetter.Requirements, hcl.Diagn
func (c *CoreWrapper) ConfiguredArtifactMetadataPublisher() (*packerregistry.Bucket, hcl.Diagnostics) {
// JSON can only configure its Bucket through Env. variables so if not in PAR mode
// we don't really care if bucket is nil or set to a bunch of zero values.
if !env.InPARMode() {
if !env.IsPAREnabled() {
return nil, hcl.Diagnostics{
&hcl.Diagnostic{
Summary: "Publishing build artifacts to HCP Packer Registry not enabled",
Expand Down
18 changes: 18 additions & 0 deletions hcl2template/testdata/build/hcp_packer_registry.pkr.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// starts resources to provision them.
build {
hcp_packer_registry {
description = <<EOT
Some description
EOT
labels = {
"foo" = "bar"
}
}

sources = [
"source.virtualbox-iso.ubuntu-1204",
]
}

source "virtualbox-iso" "ubuntu-1204" {
}
60 changes: 37 additions & 23 deletions hcl2template/types.build.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ const (
buildPostProcessorLabel = "post-processor"

buildPostProcessorsLabel = "post-processors"

buildHCPPackerRegistryLabel = "hcp_packer_registry"
)

var buildSchema = &hcl.BodySchema{
Expand All @@ -32,6 +34,7 @@ var buildSchema = &hcl.BodySchema{
{Type: buildErrorCleanupProvisionerLabel, LabelNames: []string{"type"}},
{Type: buildPostProcessorLabel, LabelNames: []string{"type"}},
{Type: buildPostProcessorsLabel, LabelNames: []string{}},
{Type: buildHCPPackerRegistryLabel},
},
}

Expand All @@ -58,6 +61,8 @@ type BuildBlock struct {
// call for example.
Description string

HCPPackerRegistry *HCPPackerRegistryBlock

// Sources is the list of sources that we want to start in this build block.
Sources []SourceUseBlock

Expand Down Expand Up @@ -99,27 +104,6 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB
build.Name = b.Name
build.Description = b.Description

// TODO if hcp_packer_registry block defined create bucket using the config specified otherwise
// load defaults from ENV
// if config has values => override the env.
if env.InPARMode() {
var err error
cfg.bucket, err = packerregistry.NewBucketWithIteration(packerregistry.IterationOptions{
TemplateBaseDir: cfg.Basedir,
})
cfg.bucket.LoadDefaultSettingsFromEnv()
if build.Name != "" {
cfg.bucket.Slug = build.Name
}
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unable to create a valid bucket object for HCP Packer Registry",
Detail: fmt.Sprintf("%s", err),
Severity: hcl.DiagError,
})
}
}

for _, buildFrom := range b.FromSources {
ref := sourceRefFromString(buildFrom)

Expand All @@ -132,14 +116,14 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB
Detail: "A " + sourceLabel + " type is made of three parts that are" +
"split by a dot `.`; each part must start with a letter and " +
"may contain only letters, digits, underscores, and dashes." +
"A valid source reference looks like: `source.type.name`", Subject: block.DefRange.Ptr(),
"A valid source reference looks like: `source.type.name`",
Subject: block.DefRange.Ptr(),
})
continue
}

// source with no body
build.Sources = append(build.Sources, SourceUseBlock{SourceRef: ref})
cfg.bucket.RegisterBuildForComponent(ref.String())
}

body = b.Config
Expand All @@ -150,6 +134,13 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB
}
for _, block := range content.Blocks {
switch block.Type {
case buildHCPPackerRegistryLabel:
hcpPackerRegistry, moreDiags := p.decodeHCPRegistry(block)
diags = append(diags, moreDiags...)
if moreDiags.HasErrors() {
nywilken marked this conversation as resolved.
Show resolved Hide resolved
continue
}
build.HCPPackerRegistry = hcpPackerRegistry
case sourceLabel:
ref, moreDiags := p.decodeBuildSource(block)
diags = append(diags, moreDiags...)
Expand Down Expand Up @@ -211,5 +202,28 @@ func (p *Parser) decodeBuildConfig(block *hcl.Block, cfg *PackerConfig) (*BuildB
}
}

if build.HCPPackerRegistry != nil || env.IsPAREnabled() {
var err error
cfg.bucket, err = packerregistry.NewBucketWithIteration(packerregistry.IterationOptions{
TemplateBaseDir: cfg.Basedir,
})
if err != nil {
diags = append(diags, &hcl.Diagnostic{
Summary: "Unable to create a valid bucket object for HCP Packer Registry",
Detail: fmt.Sprintf("%s", err),
Severity: hcl.DiagError,
})
}
cfg.bucket.LoadDefaultSettingsFromEnv()
if cfg.bucket.Slug == "" && build.Name != "" {
cfg.bucket.Slug = build.Name
}
build.HCPPackerRegistry.WriteBucketConfig(cfg.bucket)

for _, source := range build.Sources {
cfg.bucket.RegisterBuildForComponent(source.String())
}
}

return build, diags
}
42 changes: 42 additions & 0 deletions hcl2template/types.build.hcp_packer_registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package hcl2template

import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/gohcl"
packerregistry "github.com/hashicorp/packer/internal/packer_registry"
)

type HCPPackerRegistryBlock struct {
Description string
Labels map[string]string

HCL2Ref HCL2Ref
}

func (b *HCPPackerRegistryBlock) WriteBucketConfig(bucket *packerregistry.Bucket) {
sylviamoss marked this conversation as resolved.
Show resolved Hide resolved
if b == nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

💯

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that I learned this was possible 😆

return
}
bucket.Description = b.Description
bucket.Labels = b.Labels
}

func (p *Parser) decodeHCPRegistry(block *hcl.Block) (*HCPPackerRegistryBlock, hcl.Diagnostics) {
par := &HCPPackerRegistryBlock{}
body := block.Body

var b struct {
Description string `hcl:"description,optional"`
Labels map[string]string `hcl:"labels,optional"`
Config hcl.Body `hcl:",remain"`
}
diags := gohcl.DecodeBody(body, nil, &b)
if diags.HasErrors() {
return nil, diags
}

par.Description = b.Description
par.Labels = b.Labels

return par, diags
}
35 changes: 35 additions & 0 deletions hcl2template/types.build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,41 @@ func TestParse_build(t *testing.T) {
},
false,
},
{"hcp packer registry build",
defaultParser,
parseTestArgs{"testdata/build/hcp_packer_registry.pkr.hcl", nil, nil},
&PackerConfig{
CorePackerVersionString: lockedVersion,
Basedir: filepath.Join("testdata", "build"),
Sources: map[SourceRef]SourceBlock{
refVBIsoUbuntu1204: {Type: "virtualbox-iso", Name: "ubuntu-1204"},
},
Builds: Builds{
&BuildBlock{
HCPPackerRegistry: &HCPPackerRegistryBlock{
Description: "Some description\n",
Labels: map[string]string{"foo": "bar"},
},
Sources: []SourceUseBlock{
{
SourceRef: refVBIsoUbuntu1204,
},
},
},
},
},
false, false,
[]packersdk.Build{
&packer.CoreBuild{
Type: "virtualbox-iso.ubuntu-1204",
Prepared: true,
Builder: emptyMockBuilder,
Provisioners: []packer.CoreBuildProvisioner{},
PostProcessors: [][]packer.CoreBuildPostProcessor{},
},
},
false,
},
}
testParse(t, tests)
}
100 changes: 60 additions & 40 deletions internal/packer_registry/client.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
package packer_registry

import (
"errors"
"fmt"

packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/preview/2021-04-30/client/packer_service"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/organization_service"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/project_service"
organizationSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/organization_service"
projectSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/client/project_service"
rmmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/models"
"github.com/hashicorp/hcp-sdk-go/httpclient"
"github.com/hashicorp/packer/internal/packer_registry/env"
)

// ClientConfig specifies configuration for the client that interacts with HCP
type ClientConfig struct {
ClientID string
ClientSecret string
// Client is an HCP client capable of making requests on behalf of a service principal
type Client struct {
Packer packerSvc.ClientService
Organization organizationSvc.ClientService
Project projectSvc.ClientService

// OrganizationID is the organization unique identifier on HCP.
OrganizationID string
Expand All @@ -21,58 +24,75 @@ type ClientConfig struct {
ProjectID string
}

func (cfg ClientConfig) Validate() error {
if cfg.OrganizationID == "" {
return &ClientError{
// NewClient returns an authenticated client to a HCP Packer Registry.
// Client authentication requires the following environment variables be set HCP_CLIENT_ID and HCP_CLIENT_SECRET.
// Upon error a HCPClientError will be returned.
func NewClient() (*Client, error) {
if !env.HasHCPCredentials() {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: errors.New(`no valid HCP Organization ID found, check that HCP_PACKER_REGISTRY is in the format "HCP_ORG_ID/HCP_PROJ_ID"`),
Err: fmt.Errorf("the client authentication requires both HCP_CLIENT_ID and HCP_CLIENT_SECRET environment variables to be set"),
}

}

if cfg.ProjectID == "" {
return &ClientError{
cl, err := httpclient.New(httpclient.Config{})
if err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: errors.New(`no valid HCP Project ID found, check that HCP_PACKER_REGISTRY is in the format "HCP_ORG_ID/HCP_PROJ_ID"`),
Err: err,
}
}

return nil
}

// Client is an HCP client capable of making requests on behalf of a service principal
type Client struct {
Config ClientConfig

Organization organization_service.ClientService
Project project_service.ClientService
Packer packerSvc.ClientService
}
client := &Client{
Packer: packerSvc.New(cl, nil),
Organization: organizationSvc.New(cl, nil),
Project: projectSvc.New(cl, nil),
}

// NewClient returns an authenticated client to a HCP Packer Registry.
// Client authentication requires the following environment variables be set HCP_CLIENT_ID, HCP_CLIENT_SECRET, and HCP_PACKER_REGISTRY.
// if not explicitly provided via a valid ClientConfig cfg.
// Upon error a HCPClientError will be returned.
func NewClient(cfg ClientConfig) (*Client, error) {
if err := cfg.Validate(); err != nil {
if err := client.loadOrganizationID(); err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
}

cl, err := httpclient.New(httpclient.Config{})
if err != nil {
if err := client.loadProjectID(); err != nil {
return nil, &ClientError{
StatusCode: InvalidClientConfig,
Err: err,
}
}

svc := packerSvc.New(cl, nil)
return &Client{
Packer: svc,
Config: cfg,
}, nil
return client, nil
}

func (c *Client) loadOrganizationID() error {
// Get the organization ID.
listOrgParams := organizationSvc.NewOrganizationServiceListParams()
listOrgResp, err := c.Organization.OrganizationServiceList(listOrgParams, nil)
if err != nil {
return fmt.Errorf("unable to fetch organization list: %v", err)
}
orgLen := len(listOrgResp.Payload.Organizations)
nywilken marked this conversation as resolved.
Show resolved Hide resolved
if orgLen != 1 {
return fmt.Errorf("unexpected number of organizations: expected 1, actual: %v", orgLen)
}
c.OrganizationID = listOrgResp.Payload.Organizations[0].ID
return nil
}

func (c *Client) loadProjectID() error {
// Get the project using the organization ID.
listProjParams := projectSvc.NewProjectServiceListParams()
listProjParams.ScopeID = &c.OrganizationID
scopeType := string(rmmodels.HashicorpCloudResourcemanagerResourceIDResourceTypeORGANIZATION)
listProjParams.ScopeType = &scopeType
listProjResp, err := c.Project.ProjectServiceList(listProjParams, nil)
if err != nil {
return fmt.Errorf("unable to fetch project id: %v", err)
}
if len(listProjResp.Payload.Projects) > 1 {
return fmt.Errorf("this version of Packer does not support multiple projects")
}
c.ProjectID = listProjResp.Payload.Projects[0].ID
return nil
}
13 changes: 6 additions & 7 deletions internal/packer_registry/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,15 @@ func HasClientSecret() bool {
return ok
}

func HasPackerRegistryDestionation() bool {
_, ok := os.LookupEnv(HCPPackerRegistry)
return ok
}

func HasPackerRegistryBucket() bool {
_, ok := os.LookupEnv(HCPPackerBucket)
return ok
}

func InPARMode() bool {
func HasHCPCredentials() bool {
checks := []func() bool{
HasClientID,
HasClientSecret,
HasPackerRegistryDestionation,
}

for _, check := range checks {
Expand All @@ -37,3 +31,8 @@ func InPARMode() bool {

return true
}

func IsPAREnabled() bool {
_, ok := os.LookupEnv(HCPPackerRegistry)
sylviamoss marked this conversation as resolved.
Show resolved Hide resolved
return ok
}
Loading