//go:generate struct-markdown //go:generate mapstructure-to-hcl2 -type Config package hyperone import ( "errors" "fmt" "io/ioutil" "os" "time" "github.com/hashicorp/packer-plugin-sdk/common" "github.com/hashicorp/packer-plugin-sdk/communicator" "github.com/hashicorp/packer-plugin-sdk/json" "github.com/hashicorp/packer-plugin-sdk/multistep" packersdk "github.com/hashicorp/packer-plugin-sdk/packer" "github.com/hashicorp/packer-plugin-sdk/template/config" "github.com/hashicorp/packer-plugin-sdk/template/interpolate" "github.com/hashicorp/packer-plugin-sdk/uuid" "github.com/mitchellh/go-homedir" "github.com/mitchellh/mapstructure" ) const ( configPath = "~/.h1-cli/conf.json" tokenEnv = "HYPERONE_TOKEN" defaultDiskType = "ssd" defaultImageService = "564639bc052c084e2f2e3266" defaultStateTimeout = 5 * time.Minute defaultUserName = "guru" ) type Config struct { common.PackerConfig `mapstructure:",squash"` Comm communicator.Config `mapstructure:",squash"` // Custom API endpoint URL, compatible with HyperOne. // It can also be specified via environment variable HYPERONE_API_URL. APIURL string `mapstructure:"api_url" required:"false"` // The authentication token used to access your account. // This can be either a session token or a service account token. // If not defined, the builder will attempt to find it in the following order: Token string `mapstructure:"token" required:"true"` // The id or name of the project. This field is required // only if using session tokens. It should be skipped when using service // account authentication. Project string `mapstructure:"project" required:"true"` // Login (an e-mail) on HyperOne platform. Set this // if you want to fetch the token by SSH authentication. TokenLogin string `mapstructure:"token_login" required:"false"` // Timeout for waiting on the API to complete // a request. Defaults to 5m. StateTimeout time.Duration `mapstructure:"state_timeout" required:"false"` // ID or name of the image to launch server from. SourceImage string `mapstructure:"source_image" required:"true"` // The name of the resulting image. Defaults to // `packer-{{timestamp}}` // (see configuration templates for more info). ImageName string `mapstructure:"image_name" required:"false"` // The description of the resulting image. ImageDescription string `mapstructure:"image_description" required:"false"` // Key/value pair tags to add to the created image. ImageTags map[string]string `mapstructure:"image_tags" required:"false"` // Same as [`image_tags`](#image_tags) but defined as a singular repeatable // block containing a `key` and a `value` field. In HCL2 mode the // [`dynamic_block`](/docs/configuration/from-1.5/expressions#dynamic-blocks) // will allow you to create those programatically. ImageTag config.KeyValues `mapstructure:"image_tag" required:"false"` // The service of the resulting image. ImageService string `mapstructure:"image_service" required:"false"` // ID or name of the type this server should be created with. VmType string `mapstructure:"vm_type" required:"true"` // The name of the created server. VmName string `mapstructure:"vm_name" required:"false"` // Key/value pair tags to add to the created server. VmTags map[string]string `mapstructure:"vm_tags" required:"false"` // Same as [`vm_tags`](#vm_tags) but defined as a singular repeatable block // containing a `key` and a `value` field. In HCL2 mode the // [`dynamic_block`](/docs/configuration/from-1.5/expressions#dynamic-blocks) // will allow you to create those programatically. VmTag config.NameValues `mapstructure:"vm_tag" required:"false"` // The name of the created disk. DiskName string `mapstructure:"disk_name" required:"false"` // The type of the created disk. Defaults to ssd. DiskType string `mapstructure:"disk_type" required:"false"` // Size of the created disk, in GiB. DiskSize float32 `mapstructure:"disk_size" required:"true"` // The ID of the network to attach to the created server. Network string `mapstructure:"network" required:"false"` // The ID of the private IP within chosen network // that should be assigned to the created server. PrivateIP string `mapstructure:"private_ip" required:"false"` // The ID of the public IP that should be assigned to // the created server. If network is chosen, the public IP will be associated // with server's private IP. PublicIP string `mapstructure:"public_ip" required:"false"` // Custom service of public network adapter. // Can be useful when using custom api_url. Defaults to public. PublicNetAdpService string `mapstructure:"public_netadp_service" required:"false"` ChrootDevice string `mapstructure:"chroot_device"` ChrootDisk bool `mapstructure:"chroot_disk"` ChrootDiskSize float32 `mapstructure:"chroot_disk_size"` ChrootDiskType string `mapstructure:"chroot_disk_type"` ChrootMountPath string `mapstructure:"chroot_mount_path"` ChrootMounts [][]string `mapstructure:"chroot_mounts"` ChrootCopyFiles []string `mapstructure:"chroot_copy_files"` // How to run shell commands. This defaults to `{{.Command}}`. This may be // useful to set if you want to set environmental variables or perhaps run // it with sudo or so on. This is a configuration template where the // .Command variable is replaced with the command to be run. Defaults to // `{{.Command}}`. ChrootCommandWrapper string `mapstructure:"chroot_command_wrapper"` MountOptions []string `mapstructure:"mount_options"` MountPartition string `mapstructure:"mount_partition"` // A series of commands to execute after attaching the root volume and // before mounting the chroot. This is not required unless using // from_scratch. If so, this should include any partitioning and filesystem // creation commands. The path to the device is provided by `{{.Device}}`. PreMountCommands []string `mapstructure:"pre_mount_commands"` // As pre_mount_commands, but the commands are executed after mounting the // root device and before the extra mount and copy steps. The device and // mount path are provided by `{{.Device}}` and `{{.MountPath}}`. PostMountCommands []string `mapstructure:"post_mount_commands"` // List of SSH keys by name or id to be added // to the server on launch. SSHKeys []string `mapstructure:"ssh_keys" required:"false"` // User data to launch with the server. Packer will not // automatically wait for a user script to finish before shutting down the // instance, this must be handled in a provisioner. UserData string `mapstructure:"user_data" required:"false"` ctx interpolate.Context } func (c *Config) Prepare(raws ...interface{}) ([]string, error) { var md mapstructure.Metadata err := config.Decode(c, &config.DecodeOpts{ Metadata: &md, Interpolate: true, InterpolateContext: &c.ctx, InterpolateFilter: &interpolate.RenderFilter{ Exclude: []string{ "run_command", "chroot_command_wrapper", "post_mount_commands", "pre_mount_commands", "mount_path", }, }, }, raws...) if err != nil { return nil, err } cliConfig, err := loadCLIConfig() if err != nil { return nil, err } // Defaults if c.Comm.SSHUsername == "" { c.Comm.SSHUsername = defaultUserName } if c.Comm.SSHTimeout == 0 { c.Comm.SSHTimeout = 10 * time.Minute } if c.APIURL == "" { c.APIURL = os.Getenv("HYPERONE_API_URL") } if c.Token == "" { c.Token = os.Getenv(tokenEnv) if c.Token == "" { c.Token = cliConfig.Profile.APIKey } // Fetching token by SSH is available only for the default API endpoint if c.TokenLogin != "" && c.APIURL == "" { c.Token, err = fetchTokenBySSH(c.TokenLogin) if err != nil { return nil, err } } } if c.Project == "" { c.Project = cliConfig.Profile.Project.ID } if c.StateTimeout == 0 { c.StateTimeout = defaultStateTimeout } if c.ImageName == "" { name, err := interpolate.Render("packer-{{timestamp}}", nil) if err != nil { return nil, err } c.ImageName = name } if c.ImageService == "" { c.ImageService = defaultImageService } if c.VmName == "" { c.VmName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) } if c.DiskType == "" { c.DiskType = defaultDiskType } if c.PublicNetAdpService == "" { c.PublicNetAdpService = "public" } if c.ChrootCommandWrapper == "" { c.ChrootCommandWrapper = "{{.Command}}" } if c.ChrootDiskSize == 0 { c.ChrootDiskSize = c.DiskSize } if c.ChrootDiskType == "" { c.ChrootDiskType = c.DiskType } if c.ChrootMountPath == "" { path, err := interpolate.Render("/mnt/packer-hyperone-volumes/{{timestamp}}", nil) if err != nil { return nil, err } c.ChrootMountPath = path } if c.ChrootMounts == nil { c.ChrootMounts = make([][]string, 0) } if len(c.ChrootMounts) == 0 { c.ChrootMounts = [][]string{ {"proc", "proc", "/proc"}, {"sysfs", "sysfs", "/sys"}, {"bind", "/dev", "/dev"}, {"devpts", "devpts", "/dev/pts"}, {"binfmt_misc", "binfmt_misc", "/proc/sys/fs/binfmt_misc"}, } } if c.ChrootCopyFiles == nil { c.ChrootCopyFiles = []string{"/etc/resolv.conf"} } if c.MountPartition == "" { c.MountPartition = "1" } // Validation var errs *packersdk.MultiError errs = packersdk.MultiErrorAppend(errs, c.ImageTag.CopyOn(&c.ImageTags)...) errs = packersdk.MultiErrorAppend(errs, c.VmTag.CopyOn(&c.VmTags)...) if es := c.Comm.Prepare(&c.ctx); len(es) > 0 { errs = packersdk.MultiErrorAppend(errs, es...) } if c.Token == "" { errs = packersdk.MultiErrorAppend(errs, errors.New("token is required")) } if c.VmType == "" { errs = packersdk.MultiErrorAppend(errs, errors.New("vm type is required")) } if c.DiskSize == 0 { errs = packersdk.MultiErrorAppend(errs, errors.New("disk size is required")) } if c.SourceImage == "" { errs = packersdk.MultiErrorAppend(errs, errors.New("source image is required")) } if c.ChrootDisk { if len(c.PreMountCommands) == 0 { errs = packersdk.MultiErrorAppend(errs, errors.New("pre-mount commands are required for chroot disk")) } } for _, mounts := range c.ChrootMounts { if len(mounts) != 3 { errs = packersdk.MultiErrorAppend( errs, errors.New("each chroot_mounts entry should have three elements")) break } } if errs != nil && len(errs.Errors) > 0 { return nil, errs } packersdk.LogSecretFilter.Set(c.Token) return nil, nil } type cliConfig struct { Profile struct { APIKey string `json:"apiKey"` Project struct { ID string `json:"id"` } `json:"project"` } `json:"profile"` } func loadCLIConfig() (cliConfig, error) { path, err := homedir.Expand(configPath) if err != nil { return cliConfig{}, err } _, err = os.Stat(path) if err != nil { // Config not found return cliConfig{}, nil } content, err := ioutil.ReadFile(path) if err != nil { return cliConfig{}, err } var c cliConfig err = json.Unmarshal(content, &c) if err != nil { return cliConfig{}, err } return c, nil } func getPublicIP(state multistep.StateBag) (string, error) { return state.Get("public_ip").(string), nil }