//go:generate mapstructure-to-hcl2 -type Config,CreateVNICDetails,ListImagesRequest package oci import ( "encoding/base64" "errors" "fmt" "io/ioutil" "log" "os" "os/user" "path/filepath" "strings" "github.com/hashicorp/packer/helper/communicator" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/packer-plugin-sdk/common" packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer" "github.com/hashicorp/packer/packer-plugin-sdk/template/config" "github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate" ocicommon "github.com/oracle/oci-go-sdk/common" ociauth "github.com/oracle/oci-go-sdk/common/auth" ) type CreateVNICDetails struct { // fields that can be specified under "create_vnic_details" AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false"` DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false"` DisplayName *string `mapstructure:"display_name" required:"false"` FreeformTags map[string]string `mapstructure:"tags" required:"false"` HostnameLabel *string `mapstructure:"hostname_label" required:"false"` NsgIds []string `mapstructure:"nsg_ids" required:"false"` PrivateIp *string `mapstructure:"private_ip" required:"false"` SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false"` SubnetId *string `mapstructure:"subnet_id" required:"false"` } type ListImagesRequest struct { // fields that can be specified under "base_image_filter" CompartmentId *string `mapstructure:"compartment_id"` DisplayName *string `mapstructure:"display_name"` DisplayNameSearch *string `mapstructure:"display_name_search"` OperatingSystem *string `mapstructure:"operating_system"` OperatingSystemVersion *string `mapstructure:"operating_system_version"` Shape *string `mapstructure:"shape"` } type Config struct { common.PackerConfig `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"` configProvider ocicommon.ConfigurationProvider // Instance Principals (OPTIONAL) // If set to true the following can't have non empty values // - AccessCfgFile // - AccessCfgFileAccount // - UserID // - TenancyID // - Region // - Fingerprint // - KeyFile // - PassPhrase InstancePrincipals bool `mapstructure:"use_instance_principals"` AccessCfgFile string `mapstructure:"access_cfg_file"` AccessCfgFileAccount string `mapstructure:"access_cfg_file_account"` // Access config overrides UserID string `mapstructure:"user_ocid"` TenancyID string `mapstructure:"tenancy_ocid"` Region string `mapstructure:"region"` Fingerprint string `mapstructure:"fingerprint"` KeyFile string `mapstructure:"key_file"` PassPhrase string `mapstructure:"pass_phrase"` UsePrivateIP bool `mapstructure:"use_private_ip"` AvailabilityDomain string `mapstructure:"availability_domain"` CompartmentID string `mapstructure:"compartment_ocid"` // Image BaseImageID string `mapstructure:"base_image_ocid"` BaseImageFilter ListImagesRequest `mapstructure:"base_image_filter"` ImageName string `mapstructure:"image_name"` ImageCompartmentID string `mapstructure:"image_compartment_ocid"` LaunchMode string `mapstructure:"image_launch_mode"` // Instance InstanceName *string `mapstructure:"instance_name"` InstanceTags map[string]string `mapstructure:"instance_tags"` InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags"` Shape string `mapstructure:"shape"` BootVolumeSizeInGBs int64 `mapstructure:"disk_size"` // Metadata optionally contains custom metadata key/value pairs provided in the // configuration. While this can be used to set metadata["user_data"] the explicit // "user_data" and "user_data_file" values will have precedence. // An instance's metadata can be obtained from at http://169.254.169.254 on the // launched instance. Metadata map[string]string `mapstructure:"metadata"` // UserData and UserDataFile file are both optional and mutually exclusive. UserData string `mapstructure:"user_data"` UserDataFile string `mapstructure:"user_data_file"` // Networking SubnetID string `mapstructure:"subnet_ocid"` CreateVnicDetails CreateVNICDetails `mapstructure:"create_vnic_details"` // Tagging Tags map[string]string `mapstructure:"tags"` DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags"` ctx interpolate.Context } func (c *Config) ConfigProvider() ocicommon.ConfigurationProvider { return c.configProvider } func (c *Config) Prepare(raws ...interface{}) error { // Decode from template err := config.Decode(c, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &c.ctx, }, raws...) if err != nil { return fmt.Errorf("Failed to mapstructure Config: %+v", err) } var errs *packersdk.MultiError if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { errs = packersdk.MultiErrorAppend(errs, es...) } 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.AccessCfgFile != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file"+message)) } if c.AccessCfgFileAccount != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file_account"+message)) } if c.UserID != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("user_ocid"+message)) } if c.TenancyID != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("tenancy_ocid"+message)) } if c.Region != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("region"+message)) } if c.Fingerprint != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("fingerprint"+message)) } if c.KeyFile != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("key_file"+message)) } if c.PassPhrase != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("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 configuraion 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 } } tenancyOCID, err = c.configProvider.TenancyOCID() if err != nil { return err } } else { // Determine where the SDK config is located if c.AccessCfgFile == "" { c.AccessCfgFile, err = getDefaultOCISettingsPath() if err != nil { log.Println("Default OCI settings file not found") } } if c.AccessCfgFileAccount == "" { c.AccessCfgFileAccount = "DEFAULT" } var keyContent []byte if c.KeyFile != "" { path, err := packer.ExpandUser(c.KeyFile) if err != nil { return err } // Read API signing key keyContent, err = ioutil.ReadFile(path) if err != nil { return err } } fileProvider, _ := ocicommon.ConfigurationProviderFromFileWithProfile(c.AccessCfgFile, c.AccessCfgFileAccount, c.PassPhrase) if c.Region == "" { var region string if fileProvider != nil { region, _ = fileProvider.Region() } if region == "" { c.Region = "us-phoenix-1" } } providers := []ocicommon.ConfigurationProvider{ NewRawConfigurationProvider(c.TenancyID, c.UserID, c.Region, c.Fingerprint, string(keyContent), &c.PassPhrase), } if fileProvider != nil { providers = append(providers, fileProvider) } // Load API access configuration from SDK configProvider, err := ocicommon.ComposingConfigurationProvider(providers) if err != nil { return err } if userOCID, _ := configProvider.UserOCID(); userOCID == "" { errs = packersdk.MultiErrorAppend( errs, errors.New("'user_ocid' must be specified")) } tenancyOCID, _ = configProvider.TenancyOCID() if tenancyOCID == "" { errs = packersdk.MultiErrorAppend( errs, errors.New("'tenancy_ocid' must be specified")) } if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" { errs = packersdk.MultiErrorAppend( errs, errors.New("'fingerprint' must be specified")) } if _, err := configProvider.PrivateRSAKey(); err != nil { errs = packersdk.MultiErrorAppend( errs, errors.New("'key_file' must be specified")) } c.configProvider = configProvider } if c.AvailabilityDomain == "" { errs = packersdk.MultiErrorAppend( errs, errors.New("'availability_domain' must be specified")) } if c.CompartmentID == "" && tenancyOCID != "" { c.CompartmentID = tenancyOCID } if c.ImageCompartmentID == "" { c.ImageCompartmentID = c.CompartmentID } if c.Shape == "" { errs = packersdk.MultiErrorAppend( errs, errors.New("'shape' must be specified")) } if (c.SubnetID == "") && (c.CreateVnicDetails.SubnetId == nil) { errs = packersdk.MultiErrorAppend( errs, errors.New("'subnet_ocid' must be specified")) } if c.CreateVnicDetails.SubnetId == nil { c.CreateVnicDetails.SubnetId = &c.SubnetID } else if (*c.CreateVnicDetails.SubnetId != c.SubnetID) && (c.SubnetID != "") { errs = packer.MultiErrorAppend( errs, errors.New("'create_vnic_details[subnet]' must match 'subnet_ocid' if both are specified")) } if (c.BaseImageID == "") && (c.BaseImageFilter == ListImagesRequest{}) { errs = packersdk.MultiErrorAppend( errs, errors.New("'base_image_ocid' or 'base_image_filter' must be specified")) } if c.BaseImageFilter.CompartmentId == nil { c.BaseImageFilter.CompartmentId = &c.CompartmentID } if c.BaseImageFilter.Shape == nil { c.BaseImageFilter.Shape = &c.Shape } // Validate tag lengths. TODO (hlowndes) maximum number of tags allowed. if c.Tags != nil { for k, v := range c.Tags { k = strings.TrimSpace(k) v = strings.TrimSpace(v) if len(k) > 100 { errs = packersdk.MultiErrorAppend( errs, fmt.Errorf("Tag key length too long. Maximum 100 but found %d. Key: %s", len(k), k)) } if len(k) == 0 { errs = packersdk.MultiErrorAppend( errs, errors.New("Tag key empty in config")) } if len(v) > 100 { errs = packersdk.MultiErrorAppend( errs, fmt.Errorf("Tag value length too long. Maximum 100 but found %d. Key: %s", len(v), k)) } if len(v) == 0 { errs = packersdk.MultiErrorAppend( errs, errors.New("Tag value empty in config")) } } } if c.ImageName == "" { name, err := interpolate.Render("packer-{{timestamp}}", nil) if err != nil { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("unable to parse image name: %s", err)) } else { c.ImageName = name } } // Optional UserData config if c.UserData != "" && c.UserDataFile != "" { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified.")) } else if c.UserDataFile != "" { if _, err := os.Stat(c.UserDataFile); err != nil { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile)) } } // read UserDataFile into string. if c.UserDataFile != "" { fiData, err := ioutil.ReadFile(c.UserDataFile) if err != nil { errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Problem reading user_data_file: %s", err)) } c.UserData = string(fiData) } // Test if UserData is encoded already, and if not, encode it if c.UserData != "" { if _, err := base64.StdEncoding.DecodeString(c.UserData); err != nil { log.Printf("[DEBUG] base64 encoding user data...") c.UserData = base64.StdEncoding.EncodeToString([]byte(c.UserData)) } } // Set default boot volume size to 50 if not set // Check if size set is allowed by OCI if c.BootVolumeSizeInGBs == 0 { c.BootVolumeSizeInGBs = 50 } else if c.BootVolumeSizeInGBs < 50 || c.BootVolumeSizeInGBs > 16384 { errs = packersdk.MultiErrorAppend( errs, errors.New("'disk_size' must be between 50 and 16384 GBs")) } if errs != nil && len(errs.Errors) > 0 { return errs } return nil } // getDefaultOCISettingsPath uses os/user to compute the default // config file location ($HOME/.oci/config). func getDefaultOCISettingsPath() (string, error) { u, err := user.Current() if err != nil { return "", err } if u.HomeDir == "" { return "", fmt.Errorf("Unable to determine the home directory for the current user.") } path := filepath.Join(u.HomeDir, ".oci", "config") if _, err := os.Stat(path); err != nil { return "", err } return path, nil }