github.com/hashicorp/packer@v1.14.3/internal/hcp/api/client.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  // Package api provides access to the HCP Packer Registry API.
     5  package api
     6  
     7  import (
     8  	"fmt"
     9  	"log"
    10  	"net/http"
    11  	"os"
    12  	"time"
    13  
    14  	packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/client/packer_service"
    15  	organizationSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/organization_service"
    16  	projectSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/client/project_service"
    17  	rmmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-resource-manager/stable/2019-12-10/models"
    18  	"github.com/hashicorp/hcp-sdk-go/httpclient"
    19  	"github.com/hashicorp/packer/internal/hcp/env"
    20  	"github.com/hashicorp/packer/version"
    21  )
    22  
    23  // Client is an HCP client capable of making requests on behalf of a service principal
    24  type Client struct {
    25  	Packer       packerSvc.ClientService
    26  	Organization organizationSvc.ClientService
    27  	Project      projectSvc.ClientService
    28  
    29  	// OrganizationID  is the organization unique identifier on HCP.
    30  	OrganizationID string
    31  
    32  	// ProjectID  is the project unique identifier on HCP.
    33  	ProjectID string
    34  }
    35  
    36  // NewClient returns an authenticated client to a HCP Packer Registry.
    37  // Upon error a HCPClientError will be returned.
    38  func NewClient() (*Client, error) {
    39  	hasAuth, err := env.HasHCPAuth()
    40  	if err != nil {
    41  		return nil, &ClientError{
    42  			StatusCode: InvalidClientConfig,
    43  			Err:        fmt.Errorf("Failed to check for HCP auth, error: %s", err.Error()),
    44  		}
    45  	}
    46  	if !hasAuth {
    47  		return nil, &ClientError{
    48  			StatusCode: InvalidClientConfig,
    49  			Err:        fmt.Errorf("HCP Authentication not configured, either set an HCP Client ID and secret using the environment variables %s and %s, place an HCP credential file in the default path (%s), or at a different path specified in the %s environment variable.", env.HCPClientID, env.HCPClientSecret, env.HCPDefaultCredFilePathFull, env.HCPCredFile),
    50  		}
    51  	}
    52  
    53  	hcpClientCfg := httpclient.Config{
    54  		SourceChannel: fmt.Sprintf("packer/%s", version.PackerVersion.FormattedVersion()),
    55  	}
    56  	if err := hcpClientCfg.Canonicalize(); err != nil {
    57  		return nil, &ClientError{
    58  			StatusCode: InvalidClientConfig,
    59  			Err:        err,
    60  		}
    61  	}
    62  
    63  	cl, err := httpclient.New(hcpClientCfg)
    64  	if err != nil {
    65  		return nil, &ClientError{
    66  			StatusCode: InvalidClientConfig,
    67  			Err:        err,
    68  		}
    69  	}
    70  	client := &Client{
    71  		Packer:       packerSvc.New(cl, nil),
    72  		Organization: organizationSvc.New(cl, nil),
    73  		Project:      projectSvc.New(cl, nil),
    74  	}
    75  	// A client.Config.hcpConfig is set when calling Canonicalize on basic HCP httpclient, as on line 52.
    76  	// If a user sets HCP_* env. variables they will be loaded into the client via the SDK and used for any client calls.
    77  	// For HCP_ORGANIZATION_ID and HCP_PROJECT_ID if they are both set via env. variables the call to hcpClientCfg.Connicalize()
    78  	// will automatically loaded them using the FromEnv configOption.
    79  	//
    80  	// If both values are set we should have all that we need to continue so we can returned the configured client.
    81  	if hcpClientCfg.Profile().OrganizationID != "" && hcpClientCfg.Profile().ProjectID != "" {
    82  		client.OrganizationID = hcpClientCfg.Profile().OrganizationID
    83  		client.ProjectID = hcpClientCfg.Profile().ProjectID
    84  
    85  		return client, nil
    86  	}
    87  
    88  	if client.OrganizationID == "" {
    89  		err := client.loadOrganizationID()
    90  		if err != nil {
    91  			return nil, &ClientError{
    92  				StatusCode: InvalidClientConfig,
    93  				Err:        err,
    94  			}
    95  		}
    96  	}
    97  
    98  	if client.ProjectID == "" {
    99  		err := client.loadProjectID()
   100  		if err != nil {
   101  			return nil, &ClientError{
   102  				StatusCode: InvalidClientConfig,
   103  				Err:        err,
   104  			}
   105  		}
   106  	}
   107  
   108  	return client, nil
   109  }
   110  
   111  func (c *Client) loadOrganizationID() error {
   112  	if env.HasOrganizationID() {
   113  		c.OrganizationID = os.Getenv(env.HCPOrganizationID)
   114  		return nil
   115  	}
   116  	// Get the organization ID.
   117  	listOrgParams := organizationSvc.NewOrganizationServiceListParams()
   118  	listOrgResp, err := c.Organization.OrganizationServiceList(listOrgParams, nil)
   119  	if err != nil {
   120  		return fmt.Errorf("unable to fetch organization list: %v", err)
   121  	}
   122  	orgLen := len(listOrgResp.Payload.Organizations)
   123  	if orgLen != 1 {
   124  		return fmt.Errorf("unexpected number of organizations: expected 1, actual: %v", orgLen)
   125  	}
   126  	c.OrganizationID = listOrgResp.Payload.Organizations[0].ID
   127  	return nil
   128  }
   129  
   130  func (c *Client) loadProjectID() error {
   131  	if env.HasProjectID() {
   132  		c.ProjectID = os.Getenv(env.HCPProjectID)
   133  		err := c.ValidateRegistryForProject()
   134  		if err != nil {
   135  			return fmt.Errorf("project validation for id %q responded in error: %v", c.ProjectID, err)
   136  		}
   137  		return nil
   138  	}
   139  	// Get the project using the organization ID.
   140  	listProjParams := projectSvc.NewProjectServiceListParams()
   141  	listProjParams.ScopeID = &c.OrganizationID
   142  	scopeType := string(rmmodels.HashicorpCloudResourcemanagerResourceIDResourceTypeORGANIZATION)
   143  	listProjParams.ScopeType = &scopeType
   144  	listProjResp, err := c.Project.ProjectServiceList(listProjParams, nil)
   145  
   146  	if err != nil {
   147  		//For permission errors, our service principal may not have the ability
   148  		// to see all projects for an Org; this is the case for project-level service principals.
   149  		serviceErr, ok := err.(*projectSvc.ProjectServiceListDefault)
   150  		if !ok {
   151  			return fmt.Errorf("unable to fetch project list: %v", err)
   152  		}
   153  		if serviceErr.Code() == http.StatusForbidden {
   154  			return fmt.Errorf("unable to fetch project\n\n"+
   155  				"If the provided credentials are tied to a specific project try setting the %s environment variable to one you want to use.", env.HCPProjectID)
   156  		}
   157  	}
   158  
   159  	if len(listProjResp.Payload.Projects) > 1 {
   160  		log.Printf("[WARNING] Multiple HCP projects found, will pick the oldest one by default\n"+
   161  			"To specify which project to use, set the %s environment variable to the one you want to use.", env.HCPProjectID)
   162  	}
   163  
   164  	proj, err := getOldestProject(listProjResp.Payload.Projects)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	c.ProjectID = proj.ID
   169  	return nil
   170  }
   171  
   172  // getOldestProject retrieves the oldest project from a list based on its created_at time.
   173  func getOldestProject(projects []*rmmodels.HashicorpCloudResourcemanagerProject) (*rmmodels.HashicorpCloudResourcemanagerProject, error) {
   174  	if len(projects) == 0 {
   175  		return nil, fmt.Errorf("no project found")
   176  	}
   177  
   178  	oldestTime := time.Now()
   179  	var oldestProj *rmmodels.HashicorpCloudResourcemanagerProject
   180  	for _, proj := range projects {
   181  		projTime := time.Time(proj.CreatedAt)
   182  		if projTime.Before(oldestTime) {
   183  			oldestProj = proj
   184  			oldestTime = projTime
   185  		}
   186  	}
   187  	return oldestProj, nil
   188  }
   189  
   190  // ValidateRegistryForProject validates that there is an active registry associated to the configured organization and project ids.
   191  // A successful validation will result in a nil response. All other response represent an invalid registry error request or a registry not found error.
   192  func (c *Client) ValidateRegistryForProject() error {
   193  	params := packerSvc.NewPackerServiceGetRegistryParams()
   194  	params.LocationOrganizationID = c.OrganizationID
   195  	params.LocationProjectID = c.ProjectID
   196  
   197  	resp, err := c.Packer.PackerServiceGetRegistry(params, nil)
   198  	if err != nil {
   199  		return err
   200  	}
   201  
   202  	if resp.GetPayload().Registry == nil {
   203  		return fmt.Errorf("No active HCP Packer registry was found for the organization %q and project %q", c.OrganizationID, c.ProjectID)
   204  	}
   205  
   206  	return nil
   207  
   208  }