// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 // Package api provides access to the HCP Packer Registry API. package api import ( "fmt" "log" "net/http" "os" "time" packerSvc "github.com/hashicorp/hcp-sdk-go/clients/cloud-packer-service/stable/2023-01-01/client/packer_service" 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" "github.com/hashicorp/hcp-sdk-go/httpclient" "github.com/hashicorp/packer/internal/hcp/env" "github.com/hashicorp/packer/version" ) // 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. // Upon error a HCPClientError will be returned. func NewClient() (*Client, error) { hasAuth, err := env.HasHCPAuth() if err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: fmt.Errorf("Failed to check for HCP auth, error: %s", err.Error()), } } if !hasAuth { return nil, &ClientError{ StatusCode: InvalidClientConfig, 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), } } hcpClientCfg := httpclient.Config{ SourceChannel: fmt.Sprintf("packer/%s", version.PackerVersion.FormattedVersion()), } if err := hcpClientCfg.Canonicalize(); err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: err, } } cl, err := httpclient.New(hcpClientCfg) if err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: err, } } client := &Client{ Packer: packerSvc.New(cl, nil), Organization: organizationSvc.New(cl, nil), Project: projectSvc.New(cl, nil), } // 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. if hcpClientCfg.Profile().OrganizationID != "" && hcpClientCfg.Profile().ProjectID != "" { client.OrganizationID = hcpClientCfg.Profile().OrganizationID client.ProjectID = hcpClientCfg.Profile().ProjectID return client, nil } if client.OrganizationID == "" { err := client.loadOrganizationID() if err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: err, } } } if client.ProjectID == "" { err := client.loadProjectID() if err != nil { return nil, &ClientError{ StatusCode: InvalidClientConfig, Err: err, } } } return client, nil } func (c *Client) loadOrganizationID() error { if env.HasOrganizationID() { c.OrganizationID = os.Getenv(env.HCPOrganizationID) return nil } // 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 { if env.HasProjectID() { c.ProjectID = os.Getenv(env.HCPProjectID) err := c.ValidateRegistryForProject() if err != nil { return fmt.Errorf("project validation for id %q responded in error: %v", c.ProjectID, err) } return nil } // 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 { //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. serviceErr, ok := err.(*projectSvc.ProjectServiceListDefault) if !ok { return fmt.Errorf("unable to fetch project list: %v", err) } if serviceErr.Code() == http.StatusForbidden { return fmt.Errorf("unable to fetch project\n\n"+ "If the provided credentials are tied to a specific project try setting the %s environment variable to one you want to use.", env.HCPProjectID) } } 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) } proj, err := getOldestProject(listProjResp.Payload.Projects) if err != nil { return err } c.ProjectID = proj.ID return nil } // getOldestProject retrieves the oldest project from a list based on its created_at time. func getOldestProject(projects []*rmmodels.HashicorpCloudResourcemanagerProject) (*rmmodels.HashicorpCloudResourcemanagerProject, error) { if len(projects) == 0 { return nil, fmt.Errorf("no project found") } oldestTime := time.Now() var oldestProj *rmmodels.HashicorpCloudResourcemanagerProject for _, proj := range projects { projTime := time.Time(proj.CreatedAt) if projTime.Before(oldestTime) { oldestProj = proj oldestTime = projTime } } return oldestProj, nil } // 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. func (c *Client) ValidateRegistryForProject() error { params := packerSvc.NewPackerServiceGetRegistryParams() params.LocationOrganizationID = c.OrganizationID params.LocationProjectID = c.ProjectID resp, err := c.Packer.PackerServiceGetRegistry(params, nil) if err != nil { return err } if resp.GetPayload().Registry == nil { return fmt.Errorf("No active HCP Packer registry was found for the organization %q and project %q", c.OrganizationID, c.ProjectID) } return nil }