hcp: support HCP_PROJECT_ID environment variable

With HCP supporting multi-projects now, Packer needs to take it into
account when picking a project from an organisation.

This commit adds two cases:

1. multiple projects are defined, none is supplied through
   HCP_PROJECT_ID: in this case we will default to the oldest project
   defined for the organisation.

2. we supply HCP_PROJECT_ID: in this case, we pick the project with the
   corresponding ID, and use it for publishing metadata.
pull/12454/head
Lucas Bajolet 3 years ago committed by Lucas Bajolet
parent 3f4e49e847
commit 925cb5e541

@ -6,10 +6,14 @@ package api
import (
"fmt"
"log"
"os"
"time"
packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2021-04-30/client/packer_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"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/models"
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/hcp/env"
@ -97,9 +101,54 @@ func (c *Client) loadProjectID() error {
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")
if env.HasProjectID() {
proj, err := findProjectByID(os.Getenv(env.HCPProjectID), listProjResp.Payload.Projects)
if err != nil {
return err
}
c.ProjectID = proj.ID
} else {
if len(listProjResp.Payload.Projects) > 1 {
log.Printf("[WARNING] Multiple HCP projects found, will pick the oldest one by default\n" +
"To specify which project to use, set the HCP_PROJECT_ID environment variable to the one you want to use.")
}
proj, err := findOldestProject(listProjResp.Payload.Projects)
if err != nil {
return err
}
c.ProjectID = proj.ID
}
c.ProjectID = listProjResp.Payload.Projects[0].ID
return nil
}
func findOldestProject(projs []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) {
if len(projs) == 0 {
return nil, fmt.Errorf("no project found")
}
proj := projs[0]
for i := 1; i < len(projs); i++ {
nxtProj := projs[i]
if time.Time(nxtProj.CreatedAt).Before(time.Time(proj.CreatedAt)) {
proj = nxtProj
}
}
return proj, nil
}
func findProjectByID(projID string, projs []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) {
for _, proj := range projs {
if proj.ID == projID {
return proj, nil
}
}
return nil, fmt.Errorf("No project %q found", projID)
}

@ -0,0 +1,163 @@
package api
import (
"testing"
"time"
"github.com/go-openapi/strfmt"
"github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/preview/2019-12-10/models"
)
func TestFindProjectID(t *testing.T) {
testcases := []struct {
Name string
ProjectID string
ProjectList []*models.HashicorpCloudResourcemanagerProject
ExpectProjectID string
ExpectErr bool
}{
{
"Only one project, project exists, success",
"test-project-exists",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "test-project-exists",
},
},
"test-project-exists",
false,
},
{
"Multiple projects, project exists, success",
"test-project-exists",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "other-project-exists",
},
{
ID: "test-project-exists",
},
},
"test-project-exists",
false,
},
{
"One project, no id match, fail",
"test-project-exists",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "other-project-exists",
},
},
"",
true,
},
{
"Multiple projects, no id match, fail",
"test-project-exists",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "other-project-exists",
},
{
ID: "yet-another-project-exists",
},
},
"",
true,
},
{
"No projects, no id match, fail",
"test-project-exists",
[]*models.HashicorpCloudResourcemanagerProject{},
"",
true,
},
}
for _, tt := range testcases {
t.Run(tt.Name, func(t *testing.T) {
proj, err := findProjectByID(tt.ProjectID, tt.ProjectList)
if (err != nil) != tt.ExpectErr {
t.Errorf("test findProjectByID, expected %t, got %t",
tt.ExpectErr,
err != nil)
}
if proj != nil && proj.ID != tt.ExpectProjectID {
t.Errorf("expected to select project %q, got %q", tt.ExpectProjectID, proj.ID)
}
})
}
}
func TestFindOldestProject(t *testing.T) {
testcases := []struct {
Name string
ProjectList []*models.HashicorpCloudResourcemanagerProject
ExpectProjectID string
ExpectErr bool
}{
{
"Only one project, project exists, success",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "test-project-exists",
},
},
"test-project-exists",
false,
},
{
"Multiple projects, pick the oldest",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "test-project-exists",
CreatedAt: strfmt.DateTime(time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC)),
},
{
ID: "test-oldest-project",
CreatedAt: strfmt.DateTime(time.Date(2022, 1, 1, 1, 0, 0, 0, time.UTC)),
},
},
"test-oldest-project",
false,
},
{
"Multiple projects, different order, pick the oldest",
[]*models.HashicorpCloudResourcemanagerProject{
{
ID: "test-oldest-project",
CreatedAt: strfmt.DateTime(time.Date(2022, 1, 1, 1, 0, 0, 0, time.UTC)),
},
{
ID: "test-project-exists",
CreatedAt: strfmt.DateTime(time.Date(2023, 1, 1, 1, 0, 0, 0, time.UTC)),
},
},
"test-oldest-project",
false,
},
{
"No projects, should error",
[]*models.HashicorpCloudResourcemanagerProject{},
"",
true,
},
}
for _, tt := range testcases {
t.Run(tt.Name, func(t *testing.T) {
proj, err := findOldestProject(tt.ProjectList)
if (err != nil) != tt.ExpectErr {
t.Errorf("test findProjectByID, expected %t, got %t",
tt.ExpectErr,
err != nil)
}
if proj != nil && proj.ID != tt.ExpectProjectID {
t.Errorf("expected to select project %q, got %q", tt.ExpectProjectID, proj.ID)
}
})
}
}

@ -9,6 +9,10 @@ import (
"strings"
)
func HasProjectID() bool {
return hasEnvVar(HCPProjectID)
}
func HasClientID() bool {
return hasEnvVar(HCPClientID)
}

@ -6,6 +6,7 @@ package env
const (
HCPClientID = "HCP_CLIENT_ID"
HCPClientSecret = "HCP_CLIENT_SECRET"
HCPProjectID = "HCP_PROJECT_ID"
HCPPackerRegistry = "HCP_PACKER_REGISTRY"
HCPPackerBucket = "HCP_PACKER_BUCKET_NAME"
HCPPackerBuildFingerprint = "HCP_PACKER_BUILD_FINGERPRINT"

@ -4,6 +4,8 @@ description: |
page_title: HCP Packer
---
-> **Note:** On May 16th 2023, HCP introduced multi-project support to the platform. In order to use multiple projects in your organization, you will need to update Packer to version 1.9.1 or above. Starting with 1.9.1, you may specify a project ID to push builds to with the `HCP_PROJECT_ID` environment variable. If no project ID is specified, Packer will pick the project with the oldest creation date. Older versions of Packer are incompatible with multi-project support on HCP, and builds will fail for HCP organizations with multiple projects on versions before 1.9.1.
# HCP Packer
The HCP Packer registry bridges the gap between image factories and image deployments, allowing development and security teams to work together to create, manage, and consume images in a centralized way.

Loading…
Cancel
Save