2023-03-02 15:37:05 -05:00
// Copyright (c) HashiCorp, Inc.
2023-08-10 18:53:29 -04:00
// SPDX-License-Identifier: BUSL-1.1
2023-03-02 15:37:05 -05:00
2022-11-07 14:16:04 -05:00
// Package api provides access to the HCP Packer Registry API.
package api
2021-08-05 09:25:19 -04:00
import (
"fmt"
2023-06-01 11:55:05 -04:00
"log"
2023-07-19 12:42:37 -04:00
"net/http"
2023-06-01 11:55:05 -04:00
"os"
"time"
2021-08-05 09:25:19 -04:00
2024-01-24 13:17:35 -05:00
packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/client/packer_service"
2023-11-15 09:26:29 -05:00
organizationSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/organization_service"
projectSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/project_service"
rmmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/models"
2021-08-05 09:25:19 -04:00
"github.com/hashicorp/hcp-sdk-go/httpclient"
2022-11-07 14:16:04 -05:00
"github.com/hashicorp/packer/internal/hcp/env"
2021-12-17 11:59:46 -05:00
"github.com/hashicorp/packer/version"
2021-08-05 09:25:19 -04:00
)
// 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
// ProjectID is the project unique identifier on HCP.
ProjectID string
}
// 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 : fmt . Errorf ( "the client authentication requires both %s and %s environment variables to be set" , env . HCPClientID , env . HCPClientSecret ) ,
}
}
2023-07-19 12:42:37 -04:00
hcpClientCfg := httpclient . Config {
2021-12-17 11:59:46 -05:00
SourceChannel : fmt . Sprintf ( "packer/%s" , version . PackerVersion . FormattedVersion ( ) ) ,
2023-07-19 12:42:37 -04:00
}
if err := hcpClientCfg . Canonicalize ( ) ; err != nil {
2021-08-05 09:25:19 -04:00
return nil , & ClientError {
StatusCode : InvalidClientConfig ,
Err : err ,
}
}
2023-07-19 12:42:37 -04:00
cl , err := httpclient . New ( hcpClientCfg )
if err != nil {
return nil , & ClientError {
StatusCode : InvalidClientConfig ,
Err : err ,
}
}
2021-08-05 09:25:19 -04:00
client := & Client {
Packer : packerSvc . New ( cl , nil ) ,
Organization : organizationSvc . New ( cl , nil ) ,
Project : projectSvc . New ( cl , nil ) ,
}
2023-07-20 16:03:43 -04:00
// A client.Config.hcpConfig is set when calling Canonicalize on basic HCP httpclient, as on line 52.
// If a user sets HCP_* env. variables they will be loaded into the client via the SDK and used for any client calls.
// For HCP_ORGANIZATION_ID and HCP_PROJECT_ID if they are both set via env. variables the call to hcpClientCfg.Connicalize()
// will automatically loaded them using the FromEnv configOption.
//
// If both values are set we should have all that we need to continue so we can returned the configured client.
2023-07-19 12:42:37 -04:00
if hcpClientCfg . Profile ( ) . OrganizationID != "" && hcpClientCfg . Profile ( ) . ProjectID != "" {
client . OrganizationID = hcpClientCfg . Profile ( ) . OrganizationID
client . ProjectID = hcpClientCfg . Profile ( ) . ProjectID
2021-08-05 09:25:19 -04:00
2023-07-19 12:42:37 -04:00
return client , nil
2021-08-05 09:25:19 -04:00
}
2023-07-19 12:42:37 -04:00
if client . OrganizationID == "" {
err := client . loadOrganizationID ( )
if err != nil {
return nil , & ClientError {
StatusCode : InvalidClientConfig ,
Err : err ,
}
2021-08-05 09:25:19 -04:00
}
}
2023-07-19 12:42:37 -04:00
if client . ProjectID == "" {
err := client . loadProjectID ( )
if err != nil {
return nil , & ClientError {
StatusCode : InvalidClientConfig ,
Err : err ,
}
}
}
2023-07-20 16:03:43 -04:00
2021-08-05 09:25:19 -04:00
return client , nil
}
func ( c * Client ) loadOrganizationID ( ) error {
2023-07-19 12:42:37 -04:00
if env . HasOrganizationID ( ) {
c . OrganizationID = os . Getenv ( env . HCPOrganizationID )
return nil
}
2021-08-05 09:25:19 -04:00
// 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 )
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 {
2023-07-19 12:42:37 -04:00
if env . HasProjectID ( ) {
c . ProjectID = os . Getenv ( env . HCPProjectID )
2023-07-20 16:03:43 -04:00
err := c . ValidateRegistryForProject ( )
if err != nil {
return fmt . Errorf ( "project validation for id %q responded in error: %v" , c . ProjectID , err )
}
2023-07-19 12:42:37 -04:00
return nil
}
2021-08-05 09:25:19 -04:00
// 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 )
2023-06-01 11:55:05 -04:00
2023-07-19 12:42:37 -04:00
if err != nil {
2023-07-20 16:03:43 -04:00
//For permission errors, our service principal may not have the ability
// to see all projects for an Org; this is the case for project-level service principals.
2023-07-19 12:42:37 -04:00
serviceErr , ok := err . ( * projectSvc . ProjectServiceListDefault )
if ! ok {
return fmt . Errorf ( "unable to fetch project list: %v" , err )
2023-06-01 11:55:05 -04:00
}
2023-07-19 12:42:37 -04:00
if serviceErr . Code ( ) == http . StatusForbidden {
return fmt . Errorf ( "unable to fetch project\n\n" +
2023-07-20 16:03:43 -04:00
"If the provided credentials are tied to a specific project try setting the %s environment variable to one you want to use." , env . HCPProjectID )
2023-06-01 11:55:05 -04:00
}
2023-07-19 12:42:37 -04:00
}
2023-06-01 11:55:05 -04:00
2023-07-19 12:42:37 -04:00
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 %s environment variable to the one you want to use." , env . HCPProjectID )
2021-08-05 09:25:19 -04:00
}
2023-06-01 11:55:05 -04:00
2023-07-19 12:42:37 -04:00
proj , err := getOldestProject ( listProjResp . Payload . Projects )
if err != nil {
return err
}
c . ProjectID = proj . ID
2021-08-05 09:25:19 -04:00
return nil
}
2023-06-01 11:55:05 -04:00
2023-07-19 12:42:37 -04:00
// getOldestProject retrieves the oldest project from a list based on its created_at time.
2023-11-15 09:26:29 -05:00
func getOldestProject ( projects [ ] * rmmodels . HashicorpCloudResourcemanagerProject ) ( * rmmodels . HashicorpCloudResourcemanagerProject , error ) {
2023-07-19 12:42:37 -04:00
if len ( projects ) == 0 {
2023-06-01 11:55:05 -04:00
return nil , fmt . Errorf ( "no project found" )
}
2023-07-19 12:42:37 -04:00
oldestTime := time . Now ( )
2023-11-15 09:26:29 -05:00
var oldestProj * rmmodels . HashicorpCloudResourcemanagerProject
2023-07-19 12:42:37 -04:00
for _ , proj := range projects {
projTime := time . Time ( proj . CreatedAt )
if projTime . Before ( oldestTime ) {
oldestProj = proj
oldestTime = projTime
2023-06-01 11:55:05 -04:00
}
}
2023-07-19 12:42:37 -04:00
return oldestProj , nil
2023-06-01 11:55:05 -04:00
}
2023-07-20 16:03:43 -04:00
// ValidateRegistryForProject validates that there is an active registry associated to the configured organization and project ids.
// A successful validation will result in a nil response. All other response represent an invalid registry error request or a registry not found error.
2024-01-24 13:17:35 -05:00
func ( c * Client ) ValidateRegistryForProject ( ) error {
2023-11-15 09:26:29 -05:00
params := packerSvc . NewPackerServiceGetRegistryParams ( )
2024-01-24 13:17:35 -05:00
params . LocationOrganizationID = c . OrganizationID
params . LocationProjectID = c . ProjectID
2023-07-20 16:03:43 -04:00
2024-01-24 13:17:35 -05:00
resp , err := c . Packer . PackerServiceGetRegistry ( params , nil )
2023-07-20 16:03:43 -04:00
if err != nil {
return err
2023-06-01 11:55:05 -04:00
}
2023-07-20 16:03:43 -04:00
if resp . GetPayload ( ) . Registry == nil {
2024-01-24 13:17:35 -05:00
return fmt . Errorf ( "No active HCP Packer registry was found for the organization %q and project %q" , c . OrganizationID , c . ProjectID )
2023-07-20 16:03:43 -04:00
}
return nil
2023-06-01 11:55:05 -04:00
}