From 0e403c74ff78a9c5788c9a730753161a7d5f6b32 Mon Sep 17 00:00:00 2001 From: yugalarora Date: Mon, 23 Jun 2025 23:52:21 +0530 Subject: [PATCH] Added support for resource principal auth --- .web-docs/components/builder/oci/README.md | 5 +- builder/oci/config.go | 67 ++++++++++++++++------ builder/oci/config.hcl2spec.go | 2 + builder/oci/config_test.go | 43 ++++++++++++++ docs/builders/oci.mdx | 5 +- example/oci_resource_principals.json | 13 +++++ example/oci_resource_principals.pkr.hcl | 17 ++++++ 7 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 example/oci_resource_principals.json create mode 100644 example/oci_resource_principals.pkr.hcl diff --git a/.web-docs/components/builder/oci/README.md b/.web-docs/components/builder/oci/README.md index 2fedae3..d2e0814 100644 --- a/.web-docs/components/builder/oci/README.md +++ b/.web-docs/components/builder/oci/README.md @@ -102,9 +102,12 @@ based on which authentication method is used. Principals](https://docs.cloud.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm) instead of User Principals. If this key is set to true, setting any one of the `access_cfg_file`, `access_cfg_file_account`, `region`, `tenancy_ocid`, `user_ocid`, `key_file`, `fingerprint`, - `pass_phrase` parameters will cause an invalid configuration error. + `pass_phrase`, or `use_resource_principals` parameters will cause an invalid configuration error. Defaults to `false`. +- `use_resource_principals` (boolean) - (Optional) Use Resource Principals for authentication. Resource Principals is a capability in Oracle Cloud Infrastructure Identity and Access Management (IAM) that allows resources (such as Functions, Data Flow applications, and API Gateways) to make service calls to other OCI services. This attribute is mutually exclusive with `access_cfg_file`, `access_cfg_file_account`, `user_ocid`, `tenancy_ocid`, `region`, `fingerprint`, `key_file`, `pass_phrase`, and `use_instance_principals`. Defaults to `false`. + To use resource principals, the resource running Packer (e.g., a Compute instance, a Function) must be part of a [Dynamic Group](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingdynamicgroups.htm) that has appropriate [Policies](https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/policygetstarted.htm) granting it permissions to manage resources (e.g., launch instances, create images). + - `access_cfg_file` (string) - The path to the [OCI config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm). This parameter is _optional_ when using token-based authentication. diff --git a/builder/oci/config.go b/builder/oci/config.go index aabb873..68375a4 100644 --- a/builder/oci/config.go +++ b/builder/oci/config.go @@ -79,6 +79,19 @@ type Config struct { // - PassPhrase InstancePrincipals bool `mapstructure:"use_instance_principals"` + // Resource Principals (OPTIONAL) + // If set to true the following can't have non empty values + // - AccessCfgFile + // - AccessCfgFileAccount + // - UserID + // - TenancyID + // - Region + // - Fingerprint + // - KeyFile + // - PassPhrase + // - InstancePrincipals + UseResourcePrincipals bool `mapstructure:"use_resource_principals"` + // If true, Packer will not create the image. Useful for setting to `true` // during a build test stage. Default `false`. SkipCreateImage bool `mapstructure:"skip_create_image" required:"false"` @@ -187,12 +200,19 @@ func (c *Config) Prepare(raws ...interface{}) error { var tenancyOCID string - if c.InstancePrincipals { - // We could go through all keys in one go and report that the below set - // of keys cannot coexist with use_instance_principals but decided to - // split them and report them seperately so that the user sees the specific - // key involved. - var message string = " cannot be present when use_instance_principals is set to true." + if c.InstancePrincipals && c.UseResourcePrincipals { + errs = packersdk.MultiErrorAppend(errs, errors.New("use_instance_principals and use_resource_principals cannot both be true")) + } + + if c.InstancePrincipals || c.UseResourcePrincipals { + var authType string + if c.InstancePrincipals { + authType = "use_instance_principals" + } else { + authType = "use_resource_principals" + } + + var message string = fmt.Sprintf(" cannot be present when %s is set to true.", authType) if c.AccessCfgFile != "" { errs = packersdk.MultiErrorAppend(errs, errors.New("access_cfg_file"+message)) } @@ -217,22 +237,33 @@ func (c *Config) Prepare(raws ...interface{}) error { if c.PassPhrase != "" { errs = packersdk.MultiErrorAppend(errs, errors.New("pass_phrase"+message)) } - // This check is used to facilitate testing. During testing a Mock struct - // is assigned to c.configProvider otherwise testing fails because Instance - // Principals cannot be obtained. + if c.configProvider == nil { - // Even though the previous configuration checks might fail we don't want - // to skip this step. It seems that the logic behind the checks in this - // file is to check everything even getting the configProvider. - c.configProvider, err = ociauth.InstancePrincipalConfigurationProvider() - if err != nil { - return err + if c.InstancePrincipals { + c.configProvider, err = ociauth.InstancePrincipalConfigurationProvider() + if err != nil { + // Surface the error if we can't get a config provider. + // This typically happens when not running on an OCI instance. + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("failed to get instance principal configuration provider: %w", err)) + } + } else { // c.UseResourcePrincipals + c.configProvider, err = ociauth.ResourcePrincipalConfigurationProvider() + if err != nil { + // Surface the error if we can't get a config provider. + // This typically happens when the required environment variables are not set. + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("failed to get resource principal configuration provider: %w", err)) + } } } - tenancyOCID, err = c.configProvider.TenancyOCID() - if err != nil { - return err + // If configProvider is still nil due to an error, we can't proceed with TenancyOCID() + if c.configProvider != nil { + tenancyOCID, err = c.configProvider.TenancyOCID() + if err != nil { + // It's possible that TenancyOCID() fails even if the provider was created (e.g. permissions) + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("failed to get tenancy OCID from provider: %w", err)) + } } + } else { // Determine where the SDK config is located if c.AccessCfgFile == "" { diff --git a/builder/oci/config.hcl2spec.go b/builder/oci/config.hcl2spec.go index 88a2fa2..b42812a 100644 --- a/builder/oci/config.hcl2spec.go +++ b/builder/oci/config.hcl2spec.go @@ -68,6 +68,7 @@ type FlatConfig struct { WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"` WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"` InstancePrincipals *bool `mapstructure:"use_instance_principals" cty:"use_instance_principals" hcl:"use_instance_principals"` + UseResourcePrincipals *bool `mapstructure:"use_resource_principals" cty:"use_resource_principals" hcl:"use_resource_principals"` SkipCreateImage *bool `mapstructure:"skip_create_image" required:"false" cty:"skip_create_image" hcl:"skip_create_image"` AccessCfgFile *string `mapstructure:"access_cfg_file" cty:"access_cfg_file" hcl:"access_cfg_file"` AccessCfgFileAccount *string `mapstructure:"access_cfg_file_account" cty:"access_cfg_file_account" hcl:"access_cfg_file_account"` @@ -173,6 +174,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false}, "winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false}, "use_instance_principals": &hcldec.AttrSpec{Name: "use_instance_principals", Type: cty.Bool, Required: false}, + "use_resource_principals": &hcldec.AttrSpec{Name: "use_resource_principals", Type: cty.Bool, Required: false}, "skip_create_image": &hcldec.AttrSpec{Name: "skip_create_image", Type: cty.Bool, Required: false}, "access_cfg_file": &hcldec.AttrSpec{Name: "access_cfg_file", Type: cty.String, Required: false}, "access_cfg_file_account": &hcldec.AttrSpec{Name: "access_cfg_file_account", Type: cty.String, Required: false}, diff --git a/builder/oci/config_test.go b/builder/oci/config_test.go index 791c636..81ac61e 100644 --- a/builder/oci/config_test.go +++ b/builder/oci/config_test.go @@ -419,6 +419,49 @@ func TestConfig(t *testing.T) { }) } + // Test cases for use_resource_principals + for _, k := range invalidKeys { + t.Run(k+"_mixed_with_use_resource_principals", func(t *testing.T) { + raw := testConfig(cfgFile) + raw["use_resource_principals"] = "true" + raw[k] = "some_random_value" + + var c Config + // We need to mock the config provider for resource principals as well, + // similar to how it's done for instance principals. + // Assuming a similar mock exists or can be created: resourcePrincipalConfigurationProviderMock + c.configProvider = instancePrincipalConfigurationProviderMock{} // Or a new mock for resource principals + + errs := c.Prepare(raw) + + if errs == nil { + t.Fatalf("Expected error when %s is mixed with use_resource_principals, but got none", k) + } + if !strings.Contains(errs.Error(), k) { + t.Errorf("Expected error message for %s to contain '%s', but got: %s", k, k, errs.Error()) + } + }) + } + + t.Run("use_instance_principals_and_use_resource_principals_both_true", func(t *testing.T) { + raw := testConfig(cfgFile) + raw["use_instance_principals"] = "true" + raw["use_resource_principals"] = "true" + + var c Config + c.configProvider = instancePrincipalConfigurationProviderMock{} // Mock provider + + errs := c.Prepare(raw) + + if errs == nil { + t.Fatal("Expected error when both use_instance_principals and use_resource_principals are true, but got none") + } + expectedErrorMsg := "use_instance_principals and use_resource_principals cannot both be true" + if !strings.Contains(errs.Error(), expectedErrorMsg) { + t.Errorf("Expected error message to contain '%s', but got: %s", expectedErrorMsg, errs.Error()) + } + }) + t.Run("InstanceOptionsAreLegacyImdsEndpointsDisabledTrue", func(t *testing.T) { raw := testConfig(cfgFile) raw["instance_options_are_legacy_imds_endpoints_disabled"] = true diff --git a/docs/builders/oci.mdx b/docs/builders/oci.mdx index b4fd02c..8f74fdf 100644 --- a/docs/builders/oci.mdx +++ b/docs/builders/oci.mdx @@ -109,9 +109,12 @@ based on which authentication method is used. Principals](https://docs.cloud.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm) instead of User Principals. If this key is set to true, setting any one of the `access_cfg_file`, `access_cfg_file_account`, `region`, `tenancy_ocid`, `user_ocid`, `key_file`, `fingerprint`, - `pass_phrase` parameters will cause an invalid configuration error. + `pass_phrase`, or `use_resource_principals` parameters will cause an invalid configuration error. Defaults to `false`. +- `use_resource_principals` (boolean) - (Optional) Use Resource Principals for authentication. Resource Principals is a capability in Oracle Cloud Infrastructure Identity and Access Management (IAM) that allows resources (such as Functions, Data Flow applications, and API Gateways) to make service calls to other OCI services. This attribute is mutually exclusive with `access_cfg_file`, `access_cfg_file_account`, `user_ocid`, `tenancy_ocid`, `region`, `fingerprint`, `key_file`, `pass_phrase`, and `use_instance_principals`. Defaults to `false`. + To use resource principals, the resource running Packer (e.g., a Compute instance, a Function) must be part of a [Dynamic Group](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/managingdynamicgroups.htm) that has appropriate [Policies](https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/policygetstarted.htm) granting it permissions to manage resources (e.g., launch instances, create images). + - `access_cfg_file` (string) - The path to the [OCI config file](https://docs.us-phoenix-1.oraclecloud.com/Content/API/Concepts/sdkconfig.htm). This parameter is _optional_ when using token-based authentication. diff --git a/example/oci_resource_principals.json b/example/oci_resource_principals.json new file mode 100644 index 0000000..99ae286 --- /dev/null +++ b/example/oci_resource_principals.json @@ -0,0 +1,13 @@ +{ + "builders":[{ + "use_resource_principals": "true", + "availability_domain": "aaaa:PHX-AD-1", + "base_image_ocid": "ocid1.image.oc1.phx.aaaaaaaa5yu6pw3riqtuhxzov7fdngi4tsteganmao54nq3pyxu3hxcuzmoa", + "compartment_ocid": "ocid1.compartment.oc1..aaa", + "image_name": "ResourcePrincipalExampleImage", + "shape": "VM.Standard2.1", + "ssh_username": "opc", + "subnet_ocid": "ocid1.subnet.oc1..aaa", + "type": "oracle-oci" + }] +} \ No newline at end of file diff --git a/example/oci_resource_principals.pkr.hcl b/example/oci_resource_principals.pkr.hcl new file mode 100644 index 0000000..f3c182e --- /dev/null +++ b/example/oci_resource_principals.pkr.hcl @@ -0,0 +1,17 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +source "oracle-oci" "resource_principal_example" { + availability_domain = "aaaa:PHX-AD-1" + base_image_ocid = "ocid1.image.oc1.phx.aaaaaaaa5yu6pw3riqtuhxzov7fdngi4tsteganmao54nq3pyxu3hxcuzmoa" + compartment_ocid = "ocid1.compartment.oc1..aaa" + image_name = "ResourcePrincipalExampleImage" + shape = "VM.Standard2.1" + subnet_ocid = "ocid1.subnet.oc1..aaa" + use_resource_principals = "true" + ssh_username = "opc" +} + +build { + sources = ["source.oracle-oci.resource_principal_example"] +} \ No newline at end of file