diff --git a/internal/hcp/api/client.go b/internal/hcp/api/client.go index 21a07156c..89d4a39b4 100644 --- a/internal/hcp/api/client.go +++ b/internal/hcp/api/client.go @@ -7,6 +7,7 @@ package api import ( "fmt" "log" + "net/http" "os" "time" @@ -44,39 +45,67 @@ func NewClient() (*Client, error) { } } - cl, err := httpclient.New(httpclient.Config{ + hcpClientCfg := httpclient.Config{ SourceChannel: fmt.Sprintf("packer/%s", version.PackerVersion.FormattedVersion()), - }) - if err != nil { + } + 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), } + //if both HCP_ORGANIZATION_ID and HCP_PROJECT_ID are set via env variables the hcpConfig may have all we need already. + if hcpClientCfg.Profile().OrganizationID != "" && hcpClientCfg.Profile().ProjectID != "" { + client.OrganizationID = hcpClientCfg.Profile().OrganizationID + client.ProjectID = hcpClientCfg.Profile().ProjectID - if err := client.loadOrganizationID(); err != nil { - return nil, &ClientError{ - StatusCode: InvalidClientConfig, - Err: err, - } + return client, nil } - if err := client.loadProjectID(); err != nil { - return nil, &ClientError{ - StatusCode: InvalidClientConfig, - Err: err, + + 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 c.OrganizationID != "" { + return nil + } + + 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) @@ -92,55 +121,62 @@ func (c *Client) loadOrganizationID() error { } func (c *Client) loadProjectID() error { + if c.ProjectID != "" { + return nil + } + if env.HasProjectID() { + c.ProjectID = os.Getenv(env.HCPProjectID) + 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 { - return fmt.Errorf("unable to fetch project id: %v", err) - } - - 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.") + if err != nil { + //For permission errors our service principle may not have the perms + // to see all projects for an Org; this is the case for project-level service principles. + serviceErr, ok := err.(*projectSvc.ProjectServiceListDefault) + if !ok { + return fmt.Errorf("unable to fetch project list: %v", err) } - - proj, err := findOldestProject(listProjResp.Payload.Projects) - if err != nil { - return 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 trying setting the %s environment variable to one you want to use.", env.HCPProjectID) } + } - c.ProjectID = proj.ID + 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 } -func findOldestProject(projs []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) { - if len(projs) == 0 { +// getOldestProject retrieves the oldest project from a list based on its created_at time. +func getOldestProject(projects []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) { + if len(projects) == 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 + oldestTime := time.Now() + var oldestProj *models.HashicorpCloudResourcemanagerProject + for _, proj := range projects { + projTime := time.Time(proj.CreatedAt) + if projTime.Before(oldestTime) { + oldestProj = proj + oldestTime = projTime } } - - return proj, nil + return oldestProj, nil } func findProjectByID(projID string, projs []*models.HashicorpCloudResourcemanagerProject) (*models.HashicorpCloudResourcemanagerProject, error) { diff --git a/internal/hcp/api/client_test.go b/internal/hcp/api/client_test.go index b1cdbff61..466b390b8 100644 --- a/internal/hcp/api/client_test.go +++ b/internal/hcp/api/client_test.go @@ -94,7 +94,7 @@ func TestFindProjectID(t *testing.T) { } } -func TestFindOldestProject(t *testing.T) { +func TestGetOldestProject(t *testing.T) { testcases := []struct { Name string ProjectList []*models.HashicorpCloudResourcemanagerProject @@ -151,7 +151,7 @@ func TestFindOldestProject(t *testing.T) { for _, tt := range testcases { t.Run(tt.Name, func(t *testing.T) { - proj, err := findOldestProject(tt.ProjectList) + proj, err := getOldestProject(tt.ProjectList) if (err != nil) != tt.ExpectErr { t.Errorf("test findProjectByID, expected %t, got %t", tt.ExpectErr, diff --git a/internal/hcp/env/env.go b/internal/hcp/env/env.go index 0cf459015..0186be167 100644 --- a/internal/hcp/env/env.go +++ b/internal/hcp/env/env.go @@ -13,6 +13,10 @@ func HasProjectID() bool { return hasEnvVar(HCPProjectID) } +func HasOrganizationID() bool { + return hasEnvVar(HCPOrganizationID) +} + func HasClientID() bool { return hasEnvVar(HCPClientID) } diff --git a/internal/hcp/env/variables.go b/internal/hcp/env/variables.go index 71258c13b..8c74b600d 100644 --- a/internal/hcp/env/variables.go +++ b/internal/hcp/env/variables.go @@ -7,6 +7,7 @@ const ( HCPClientID = "HCP_CLIENT_ID" HCPClientSecret = "HCP_CLIENT_SECRET" HCPProjectID = "HCP_PROJECT_ID" + HCPOrganizationID = "HCP_ORGANIZATION_ID" HCPPackerRegistry = "HCP_PACKER_REGISTRY" HCPPackerBucket = "HCP_PACKER_BUCKET_NAME" HCPPackerBuildFingerprint = "HCP_PACKER_BUILD_FINGERPRINT"