2020-08-14 06:04:44 -04:00
//go:generate struct-markdown
2019-10-14 10:43:59 -04:00
//go:generate mapstructure-to-hcl2 -type Config
2018-03-29 23:50:58 -04:00
package googlecomputeimport
import (
2019-03-22 09:56:02 -04:00
"context"
2020-10-27 04:11:39 -04:00
"crypto/x509"
"encoding/base64"
"encoding/pem"
2018-03-29 23:50:58 -04:00
"fmt"
2020-10-27 04:11:39 -04:00
"io/ioutil"
2018-03-29 23:50:58 -04:00
"os"
"strings"
"time"
"google.golang.org/api/compute/v1"
2020-09-20 10:18:37 -04:00
"google.golang.org/api/option"
2018-03-29 23:50:58 -04:00
"google.golang.org/api/storage/v1"
2019-12-17 05:25:56 -05:00
"github.com/hashicorp/hcl/v2/hcldec"
2019-01-22 12:34:01 -05:00
"github.com/hashicorp/packer/builder/googlecompute"
2018-03-29 23:50:58 -04:00
"github.com/hashicorp/packer/packer"
2020-11-12 17:44:02 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/common"
2020-11-19 14:54:31 -05:00
packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
2020-11-18 13:34:59 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/template/config"
2020-11-11 13:21:37 -05:00
"github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate"
2020-05-15 16:13:43 -04:00
"github.com/hashicorp/packer/post-processor/artifice"
2018-03-29 23:50:58 -04:00
"github.com/hashicorp/packer/post-processor/compress"
)
type Config struct {
common . PackerConfig ` mapstructure:",squash" `
2020-08-14 06:04:44 -04:00
//The JSON file containing your account credentials.
//If specified, the account file will take precedence over any `googlecompute` builder authentication method.
AccountFile string ` mapstructure:"account_file" required:"true" `
2020-10-01 15:39:06 -04:00
// This allows service account impersonation as per the [docs](https://cloud.google.com/iam/docs/impersonating-service-accounts).
ImpersonateServiceAccount string ` mapstructure:"impersonate_service_account" required:"false" `
2020-08-14 06:04:44 -04:00
//The project ID where the GCS bucket exists and where the GCE image is stored.
ProjectId string ` mapstructure:"project_id" required:"true" `
IAP bool ` mapstructure-to-hcl:",skip" `
//The name of the GCS bucket where the raw disk image will be uploaded.
Bucket string ` mapstructure:"bucket" required:"true" `
//The name of the GCS object in `bucket` where
//the RAW disk image will be copied for import. This is treated as a
//[template engine](/docs/templates/engine). Therefore, you
//may use user variables and template functions in this field. Defaults to
//`packer-import-{{timestamp}}.tar.gz`.
GCSObjectName string ` mapstructure:"gcs_object_name" `
//The description of the resulting image.
ImageDescription string ` mapstructure:"image_description" `
//The name of the image family to which the resulting image belongs.
ImageFamily string ` mapstructure:"image_family" `
//A list of features to enable on the guest operating system. Applicable only for bootable images. Valid
2020-10-27 04:11:39 -04:00
//values are `MULTI_IP_SUBNET`, `UEFI_COMPATIBLE`,
2020-08-14 06:04:44 -04:00
//`VIRTIO_SCSI_MULTIQUEUE` and `WINDOWS` currently.
ImageGuestOsFeatures [ ] string ` mapstructure:"image_guest_os_features" `
//Key/value pair labels to apply to the created image.
ImageLabels map [ string ] string ` mapstructure:"image_labels" `
//The unique name of the resulting image.
ImageName string ` mapstructure:"image_name" required:"true" `
//Skip removing the TAR file uploaded to the GCS
//bucket after the import process has completed. "true" means that we should
//leave it in the GCS bucket, "false" means to clean it out. Defaults to
//`false`.
SkipClean bool ` mapstructure:"skip_clean" `
VaultGCPOauthEngine string ` mapstructure:"vault_gcp_oauth_engine" `
2020-10-27 04:11:39 -04:00
//A key used to establish the trust relationship between the platform owner and the firmware. You may only specify one platform key, and it must be a valid X.509 certificate.
ImagePlatformKey string ` mapstructure:"image_platform_key" `
//A key used to establish a trust relationship between the firmware and the OS. You may specify multiple comma-separated keys for this value.
ImageKeyExchangeKey [ ] string ` mapstructure:"image_key_exchange_key" `
//A database of certificates that have been revoked and will cause the system to stop booting if a boot file is signed with one of them. You may specify single or multiple comma-separated values for this value.
ImageSignaturesDB [ ] string ` mapstructure:"image_signatures_db" `
//A database of certificates that are trusted and can be used to sign boot files. You may specify single or multiple comma-separated values for this value.
ImageForbiddenSignaturesDB [ ] string ` mapstructure:"image_forbidden_signatures_db" `
2018-03-29 23:50:58 -04:00
2020-09-20 10:18:37 -04:00
account * googlecompute . ServiceAccount
2019-01-22 09:16:58 -05:00
ctx interpolate . Context
2018-03-29 23:50:58 -04:00
}
type PostProcessor struct {
config Config
}
2019-12-17 05:25:56 -05:00
func ( p * PostProcessor ) ConfigSpec ( ) hcldec . ObjectSpec { return p . config . FlatMapstructure ( ) . HCL2Spec ( ) }
2018-03-29 23:50:58 -04:00
func ( p * PostProcessor ) Configure ( raws ... interface { } ) error {
err := config . Decode ( & p . config , & config . DecodeOpts {
2020-10-09 20:01:55 -04:00
PluginType : BuilderId ,
2018-03-29 23:50:58 -04:00
Interpolate : true ,
InterpolateContext : & p . config . ctx ,
InterpolateFilter : & interpolate . RenderFilter {
Exclude : [ ] string {
"gcs_object_name" ,
} ,
} ,
} , raws ... )
if err != nil {
return err
}
2019-01-22 09:16:58 -05:00
errs := new ( packer . MultiError )
2018-03-29 23:50:58 -04:00
// Set defaults
if p . config . GCSObjectName == "" {
p . config . GCSObjectName = "packer-import-{{timestamp}}.tar.gz"
}
// Check and render gcs_object_name
if err = interpolate . Validate ( p . config . GCSObjectName , & p . config . ctx ) ; err != nil {
errs = packer . MultiErrorAppend (
errs , fmt . Errorf ( "Error parsing gcs_object_name template: %s" , err ) )
}
2019-01-22 09:16:58 -05:00
if p . config . AccountFile != "" {
2020-10-01 15:39:06 -04:00
if p . config . VaultGCPOauthEngine != "" && p . config . ImpersonateServiceAccount != "" {
2020-09-20 10:18:37 -04:00
errs = packer . MultiErrorAppend ( errs , fmt . Errorf ( "You cannot " +
2020-10-01 15:39:06 -04:00
"specify impersonate_service_account, account_file and vault_gcp_oauth_engine at the same time" ) )
2020-09-20 10:18:37 -04:00
}
2020-04-23 17:19:39 -04:00
cfg , err := googlecompute . ProcessAccountFile ( p . config . AccountFile )
2019-07-02 19:16:13 -04:00
if err != nil {
2019-01-22 09:16:58 -05:00
errs = packer . MultiErrorAppend ( errs , err )
}
2019-10-14 10:19:17 -04:00
p . config . account = cfg
2019-01-22 09:16:58 -05:00
}
2018-03-29 23:50:58 -04:00
templates := map [ string ] * string {
2019-01-22 09:16:58 -05:00
"bucket" : & p . config . Bucket ,
"image_name" : & p . config . ImageName ,
"project_id" : & p . config . ProjectId ,
2018-03-29 23:50:58 -04:00
}
for key , ptr := range templates {
if * ptr == "" {
errs = packer . MultiErrorAppend (
errs , fmt . Errorf ( "%s must be set" , key ) )
}
}
if len ( errs . Errors ) > 0 {
return errs
}
return nil
}
2020-11-19 14:54:31 -05:00
func ( p * PostProcessor ) PostProcess ( ctx context . Context , ui packersdk . Ui , artifact packer . Artifact ) ( packer . Artifact , bool , bool , error ) {
2020-01-30 05:27:58 -05:00
generatedData := artifact . State ( "generated_data" )
if generatedData == nil {
// Make sure it's not a nil map so we can assign to it later.
generatedData = make ( map [ string ] interface { } )
}
p . config . ctx . Data = generatedData
2020-09-20 10:18:37 -04:00
var err error
2020-09-20 10:31:45 -04:00
var opts option . ClientOption
2020-10-01 15:39:06 -04:00
opts , err = googlecompute . NewClientOptionGoogle ( p . config . account , p . config . VaultGCPOauthEngine , p . config . ImpersonateServiceAccount )
2019-01-22 09:16:58 -05:00
if err != nil {
2019-04-02 19:51:58 -04:00
return nil , false , false , err
2019-01-22 09:16:58 -05:00
}
2018-03-29 23:50:58 -04:00
2020-05-15 16:13:43 -04:00
switch artifact . BuilderId ( ) {
case compress . BuilderId , artifice . BuilderId :
break
default :
err := fmt . Errorf (
"Unknown artifact type: %s\nCan only import from Compress post-processor and Artifice post-processor artifacts." ,
2018-03-29 23:50:58 -04:00
artifact . BuilderId ( ) )
2019-04-02 19:51:58 -04:00
return nil , false , false , err
2018-03-29 23:50:58 -04:00
}
p . config . GCSObjectName , err = interpolate . Render ( p . config . GCSObjectName , & p . config . ctx )
if err != nil {
2019-04-02 19:51:58 -04:00
return nil , false , false , fmt . Errorf ( "Error rendering gcs_object_name template: %s" , err )
2018-03-29 23:50:58 -04:00
}
2020-09-20 10:18:37 -04:00
rawImageGcsPath , err := UploadToBucket ( opts , ui , artifact , p . config . Bucket , p . config . GCSObjectName )
2018-03-29 23:50:58 -04:00
if err != nil {
2019-04-03 15:05:38 -04:00
return nil , false , false , err
2018-03-29 23:50:58 -04:00
}
2020-10-27 04:11:39 -04:00
shieldedVMStateConfig , err := CreateShieldedVMStateConfig ( p . config . ImageGuestOsFeatures , p . config . ImagePlatformKey , p . config . ImageKeyExchangeKey , p . config . ImageSignaturesDB , p . config . ImageForbiddenSignaturesDB )
if err != nil {
return nil , false , false , err
}
gceImageArtifact , err := CreateGceImage ( opts , ui , p . config . ProjectId , rawImageGcsPath , p . config . ImageName , p . config . ImageDescription , p . config . ImageFamily , p . config . ImageLabels , p . config . ImageGuestOsFeatures , shieldedVMStateConfig )
2018-03-29 23:50:58 -04:00
if err != nil {
2019-04-03 15:05:38 -04:00
return nil , false , false , err
2018-03-29 23:50:58 -04:00
}
2018-07-02 22:44:30 -04:00
if ! p . config . SkipClean {
2020-09-20 10:18:37 -04:00
err = DeleteFromBucket ( opts , ui , p . config . Bucket , p . config . GCSObjectName )
2018-07-02 22:44:30 -04:00
if err != nil {
2019-04-03 15:05:38 -04:00
return nil , false , false , err
2018-07-02 22:44:30 -04:00
}
}
2019-04-03 15:05:38 -04:00
return gceImageArtifact , false , false , nil
2018-03-29 23:50:58 -04:00
}
2020-10-27 04:11:39 -04:00
func FillFileContentBuffer ( certOrKeyFile string ) ( * compute . FileContentBuffer , error ) {
data , err := ioutil . ReadFile ( certOrKeyFile )
if err != nil {
err := fmt . Errorf ( "Unable to read Certificate or Key file %s" , certOrKeyFile )
return nil , err
}
shield := & compute . FileContentBuffer {
Content : base64 . StdEncoding . EncodeToString ( data ) ,
FileType : "X509" ,
}
block , _ := pem . Decode ( data )
if block == nil || block . Type != "CERTIFICATE" {
_ , err = x509 . ParseCertificate ( data )
} else {
_ , err = x509 . ParseCertificate ( block . Bytes )
}
if err != nil {
shield . FileType = "BIN"
}
return shield , nil
}
func CreateShieldedVMStateConfig ( imageGuestOsFeatures [ ] string , imagePlatformKey string , imageKeyExchangeKey [ ] string , imageSignaturesDB [ ] string , imageForbiddenSignaturesDB [ ] string ) ( * compute . InitialStateConfig , error ) {
shieldedVMStateConfig := & compute . InitialStateConfig { }
for _ , v := range imageGuestOsFeatures {
if v == "UEFI_COMPATIBLE" {
if imagePlatformKey != "" {
shieldedData , err := FillFileContentBuffer ( imagePlatformKey )
if err != nil {
return nil , err
}
shieldedVMStateConfig . Pk = shieldedData
}
for _ , v := range imageKeyExchangeKey {
shieldedData , err := FillFileContentBuffer ( v )
if err != nil {
return nil , err
}
shieldedVMStateConfig . Keks = append ( shieldedVMStateConfig . Keks , shieldedData )
}
for _ , v := range imageSignaturesDB {
shieldedData , err := FillFileContentBuffer ( v )
if err != nil {
return nil , err
}
shieldedVMStateConfig . Dbs = append ( shieldedVMStateConfig . Dbs , shieldedData )
}
for _ , v := range imageForbiddenSignaturesDB {
shieldedData , err := FillFileContentBuffer ( v )
if err != nil {
return nil , err
}
shieldedVMStateConfig . Dbxs = append ( shieldedVMStateConfig . Dbxs , shieldedData )
}
}
}
return shieldedVMStateConfig , nil
}
2020-11-19 14:54:31 -05:00
func UploadToBucket ( opts option . ClientOption , ui packersdk . Ui , artifact packer . Artifact , bucket string , gcsObjectName string ) ( string , error ) {
2020-09-20 10:31:45 -04:00
service , err := storage . NewService ( context . TODO ( ) , opts )
2018-03-29 23:50:58 -04:00
if err != nil {
return "" , err
}
ui . Say ( "Looking for tar.gz file in list of artifacts..." )
source := ""
for _ , path := range artifact . Files ( ) {
ui . Say ( fmt . Sprintf ( "Found artifact %v..." , path ) )
if strings . HasSuffix ( path , ".tar.gz" ) {
source = path
break
}
}
if source == "" {
2019-01-22 09:16:58 -05:00
return "" , fmt . Errorf ( "No tar.gz file found in list of artifacts" )
2018-03-29 23:50:58 -04:00
}
artifactFile , err := os . Open ( source )
if err != nil {
err := fmt . Errorf ( "error opening %v" , source )
return "" , err
}
ui . Say ( fmt . Sprintf ( "Uploading file %v to GCS bucket %v/%v..." , source , bucket , gcsObjectName ) )
storageObject , err := service . Objects . Insert ( bucket , & storage . Object { Name : gcsObjectName } ) . Media ( artifactFile ) . Do ( )
if err != nil {
ui . Say ( fmt . Sprintf ( "Failed to upload: %v" , storageObject ) )
return "" , err
}
2019-01-22 09:16:58 -05:00
return storageObject . SelfLink , nil
2018-03-29 23:50:58 -04:00
}
2020-11-19 14:54:31 -05:00
func CreateGceImage ( opts option . ClientOption , ui packersdk . Ui , project string , rawImageURL string , imageName string , imageDescription string , imageFamily string , imageLabels map [ string ] string , imageGuestOsFeatures [ ] string , shieldedVMStateConfig * compute . InitialStateConfig ) ( packer . Artifact , error ) {
2020-09-20 10:31:45 -04:00
service , err := compute . NewService ( context . TODO ( ) , opts )
2020-09-20 10:23:04 -04:00
2018-03-29 23:50:58 -04:00
if err != nil {
return nil , err
}
2019-01-21 18:09:29 -05:00
// Build up the imageFeatures
imageFeatures := make ( [ ] * compute . GuestOsFeature , len ( imageGuestOsFeatures ) )
for _ , v := range imageGuestOsFeatures {
imageFeatures = append ( imageFeatures , & compute . GuestOsFeature {
Type : v ,
} )
}
2018-03-29 23:50:58 -04:00
gceImage := & compute . Image {
2020-10-27 04:11:39 -04:00
Description : imageDescription ,
Family : imageFamily ,
GuestOsFeatures : imageFeatures ,
Labels : imageLabels ,
Name : imageName ,
RawDisk : & compute . ImageRawDisk { Source : rawImageURL } ,
SourceType : "RAW" ,
ShieldedInstanceInitialState : shieldedVMStateConfig ,
2018-03-29 23:50:58 -04:00
}
ui . Say ( fmt . Sprintf ( "Creating GCE image %v..." , imageName ) )
op , err := service . Images . Insert ( project , gceImage ) . Do ( )
if err != nil {
ui . Say ( "Error creating GCE image" )
return nil , err
}
ui . Say ( "Waiting for GCE image creation operation to complete..." )
for op . Status != "DONE" {
op , err = service . GlobalOperations . Get ( project , op . Name ) . Do ( )
if err != nil {
return nil , err
}
time . Sleep ( 5 * time . Second )
}
// fail if image creation operation has an error
if op . Error != nil {
var imageError string
for _ , error := range op . Error . Errors {
imageError += error . Message
}
err = fmt . Errorf ( "failed to create GCE image %s: %s" , imageName , imageError )
return nil , err
}
return & Artifact { paths : [ ] string { op . TargetLink } } , nil
}
2018-07-02 22:44:30 -04:00
2020-11-19 14:54:31 -05:00
func DeleteFromBucket ( opts option . ClientOption , ui packersdk . Ui , bucket string , gcsObjectName string ) error {
2020-09-20 10:31:45 -04:00
service , err := storage . NewService ( context . TODO ( ) , opts )
2020-09-20 10:23:04 -04:00
2018-07-02 22:44:30 -04:00
if err != nil {
return err
}
ui . Say ( fmt . Sprintf ( "Deleting import source from GCS %s/%s..." , bucket , gcsObjectName ) )
err = service . Objects . Delete ( bucket , gcsObjectName ) . Do ( )
if err != nil {
ui . Say ( fmt . Sprintf ( "Failed to delete: %v/%v" , bucket , gcsObjectName ) )
return err
}
return nil
}