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 }