diff --git a/builder/nutanix/builder.go b/builder/nutanix/builder.go index ab6eca7..5d9b019 100644 --- a/builder/nutanix/builder.go +++ b/builder/nutanix/builder.go @@ -74,6 +74,13 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) }, } + if b.config.ImageExport { + steps = append(steps, &stepExportImage{ + VMName: b.config.VMName, + ImageName: b.config.VmConfig.ImageName, + }) + } + b.runner = &multistep.BasicRunner{Steps: steps} b.runner.Run(ctx, state) @@ -85,7 +92,7 @@ func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) if imageUUID != nil { artifact := &Artifact{ Name: b.config.ImageName, - UUID: imageUUID.([]string)[0], + UUID: imageUUID.([]imageArtefact)[0].uuid, } return artifact, nil } diff --git a/builder/nutanix/config.go b/builder/nutanix/config.go index e08c070..5ef4df7 100644 --- a/builder/nutanix/config.go +++ b/builder/nutanix/config.go @@ -38,6 +38,7 @@ type Config struct { ImageDescription string `mapstructure:"image_description" json:"image_description" required:"false"` ImageCategories []Category `mapstructure:"image_categories" required:"false"` ImageDelete bool `mapstructure:"image_delete" json:"image_delete" required:"false"` + ImageExport bool `mapstructure:"image_export" json:"image_export" required:"false"` WaitTimeout time.Duration `mapstructure:"ip_wait_timeout" json:"ip_wait_timeout" required:"false"` ctx interpolate.Context diff --git a/builder/nutanix/config.hcl2spec.go b/builder/nutanix/config.hcl2spec.go index bfd5b9d..fcf0ae4 100644 --- a/builder/nutanix/config.hcl2spec.go +++ b/builder/nutanix/config.hcl2spec.go @@ -149,6 +149,7 @@ type FlatConfig struct { ImageDescription *string `mapstructure:"image_description" json:"image_description" required:"false" cty:"image_description" hcl:"image_description"` ImageCategories []FlatCategory `mapstructure:"image_categories" required:"false" cty:"image_categories" hcl:"image_categories"` ImageDelete *bool `mapstructure:"image_delete" json:"image_delete" required:"false" cty:"image_delete" hcl:"image_delete"` + ImageExport *bool `mapstructure:"image_export" json:"image_export" required:"false" cty:"image_export" hcl:"image_export"` WaitTimeout *string `mapstructure:"ip_wait_timeout" json:"ip_wait_timeout" required:"false" cty:"ip_wait_timeout" hcl:"ip_wait_timeout"` } @@ -247,6 +248,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false}, "image_categories": &hcldec.BlockListSpec{TypeName: "image_categories", Nested: hcldec.ObjectSpec((*FlatCategory)(nil).HCL2Spec())}, "image_delete": &hcldec.AttrSpec{Name: "image_delete", Type: cty.Bool, Required: false}, + "image_export": &hcldec.AttrSpec{Name: "image_export", Type: cty.Bool, Required: false}, "ip_wait_timeout": &hcldec.AttrSpec{Name: "ip_wait_timeout", Type: cty.String, Required: false}, } return s diff --git a/builder/nutanix/driver.go b/builder/nutanix/driver.go index 9a299bb..9589c50 100644 --- a/builder/nutanix/driver.go +++ b/builder/nutanix/driver.go @@ -1,6 +1,9 @@ package nutanix import ( + "crypto/tls" + "io" + "net/http" "strings" "time" @@ -25,12 +28,12 @@ type Driver interface { Create(*v3.VMIntentInput) (*nutanixInstance, error) Delete(string) error GetVM(string) (*nutanixInstance, error) - //GetImage(string) (*nutanixImage, error) GetHost(string) (*nutanixHost, error) PowerOff(string) error UploadImage(string, string, string, VmConfig) (*nutanixImage, error) DeleteImage(string) error GetImage(string) (*nutanixImage, error) + ExportImage(string) (io.ReadCloser, error) SaveVMDisk(string, int, []Category) (*nutanixImage, error) WaitForShutdown(string, <-chan struct{}) bool } @@ -666,6 +669,32 @@ func (d *NutanixDriver) GetVM(vmUUID string) (*nutanixInstance, error) { return &nutanixInstance{nutanix: *vm}, nil } +func (d *NutanixDriver) ExportImage(imageUUID string) (io.ReadCloser, error) { + customTransport := http.DefaultTransport.(*http.Transport).Clone() + customTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: d.ClusterConfig.Insecure} + + client := &http.Client{Transport: customTransport} + + url := fmt.Sprintf("https://%s:%d/api/nutanix/v3/images/%s/file", d.ClusterConfig.Endpoint, d.ClusterConfig.Port, imageUUID) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, err + } + req.SetBasicAuth(d.ClusterConfig.Username, d.ClusterConfig.Password) + + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + if resp.StatusCode != 200 { + return nil, fmt.Errorf(resp.Status) + } + + return resp.Body, nil +} + func (d *NutanixDriver) GetHost(hostUUID string) (*nutanixHost, error) { configCreds := client.Credentials{ diff --git a/builder/nutanix/step_build_vm.go b/builder/nutanix/step_build_vm.go index 921df55..c45d5ba 100644 --- a/builder/nutanix/step_build_vm.go +++ b/builder/nutanix/step_build_vm.go @@ -44,7 +44,7 @@ func (s *stepBuildVM) Run(ctx context.Context, state multistep.StateBag) multist } ui.Say("Creating Packer Builder virtual machine...") - //CreateRequest() + vmRequest, err := d.CreateRequest(config.VmConfig) if err != nil { ui.Error("Error creating virtual machine request: " + err.Error()) diff --git a/builder/nutanix/step_copy_image.go b/builder/nutanix/step_copy_image.go index 2654e13..e55dd15 100644 --- a/builder/nutanix/step_copy_image.go +++ b/builder/nutanix/step_copy_image.go @@ -9,6 +9,16 @@ import ( "github.com/hashicorp/packer-plugin-sdk/packer" ) +type imageArtefact struct { + uuid string + size int64 +} + +type diskArtefact struct { + uuid string + size int64 +} + type stepCopyImage struct { Config *Config } @@ -22,11 +32,14 @@ func (s *stepCopyImage) Run(ctx context.Context, state multistep.StateBag) multi ui.Say(fmt.Sprintf("Creating image(s) from virtual machine %s...", s.Config.VMName)) // Choose disk to replicate - looking for first "DISK" - var disksToCopy []string + var disksToCopy []diskArtefact for i := range vm.nutanix.Spec.Resources.DiskList { if *vm.nutanix.Spec.Resources.DiskList[i].DeviceProperties.DeviceType == "DISK" { - disksToCopy = append(disksToCopy, *vm.nutanix.Spec.Resources.DiskList[i].UUID) + disksToCopy = append(disksToCopy, diskArtefact{ + uuid: *vm.nutanix.Spec.Resources.DiskList[i].UUID, + size: *vm.nutanix.Spec.Resources.DiskList[i].DiskSizeBytes, + }) diskID := fmt.Sprintf("%s:%d", *vm.nutanix.Spec.Resources.DiskList[i].DeviceProperties.DiskAddress.AdapterType, *vm.nutanix.Spec.Resources.DiskList[i].DeviceProperties.DiskAddress.DeviceIndex) ui.Message("Found disk to copy: " + diskID) } @@ -39,17 +52,22 @@ func (s *stepCopyImage) Run(ctx context.Context, state multistep.StateBag) multi return multistep.ActionHalt } - var imageList []string + var imageList []imageArtefact for i, diskToCopy := range disksToCopy { - imageResponse, err := d.SaveVMDisk(diskToCopy, i, s.Config.ImageCategories) + imageResponse, err := d.SaveVMDisk(diskToCopy.uuid, i, s.Config.ImageCategories) if err != nil { ui.Error("Image creation failed: " + err.Error()) state.Put("error", err) return multistep.ActionHalt } - imageList = append(imageList, *imageResponse.image.Metadata.UUID) + + imageList = append(imageList, imageArtefact{ + uuid: *imageResponse.image.Metadata.UUID, + size: diskToCopy.size, + }) + ui.Message(fmt.Sprintf("Image successfully created: %s (%s)", *imageResponse.image.Spec.Name, *imageResponse.image.Metadata.UUID)) } @@ -68,14 +86,14 @@ func (s *stepCopyImage) Cleanup(state multistep.StateBag) { if imgUUID, ok := state.GetOk("image_uuid"); ok { ui.Say(fmt.Sprintf("Deleting image(s) %s...", s.Config.ImageName)) - for _, image := range imgUUID.([]string) { + for _, image := range imgUUID.([]imageArtefact) { - err := d.DeleteImage(image) + err := d.DeleteImage(image.uuid) if err != nil { ui.Error("An error occurred while deleting image") return } else { - ui.Message(fmt.Sprintf("Image successfully deleted (%s)", image)) + ui.Message(fmt.Sprintf("Image successfully deleted (%s)", image.uuid)) } } } diff --git a/builder/nutanix/step_export_image.go b/builder/nutanix/step_export_image.go new file mode 100644 index 0000000..d63e8bb --- /dev/null +++ b/builder/nutanix/step_export_image.go @@ -0,0 +1,101 @@ +package nutanix + +import ( + "context" + "fmt" + "io" + "os" + "os/signal" + "syscall" + + "github.com/hashicorp/packer-plugin-sdk/multistep" + "github.com/hashicorp/packer-plugin-sdk/packer" +) + +type stepExportImage struct { + VMName string + ImageName string +} + +func (s *stepExportImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { + ui := state.Get("ui").(packer.Ui) + imageList := state.Get("image_uuid").([]imageArtefact) + d := state.Get("driver").(Driver) + // vm, _ := d.GetVM(vmUUID) + + ui.Say(fmt.Sprintf("Exporting image(s) from virtual machine %s...", s.VMName)) + + // Create a channel to receive signals + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + for index, imageToExport := range imageList { + + name := s.ImageName + if index > 0 { + name = fmt.Sprintf("%s-disk%d", name, index+1) + } + + file, err := d.ExportImage(imageToExport.uuid) + if err != nil { + ui.Error("Image export failed: " + err.Error()) + state.Put("error", err) + return multistep.ActionHalt + } + defer file.Close() + + toRead := ui.TrackProgress(name, 0, imageToExport.size, file) + + tempDestinationPath := name + ".tmp" + + f, err := os.OpenFile(tempDestinationPath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return multistep.ActionHalt + } + + // Use a goroutine to copy the data, so that we can + // interrupt it if necessary + copyDone := make(chan bool) + go func() { + io.Copy(f, toRead) + copyDone <- true + }() + + select { + case <-copyDone: + toRead.Close() + + // Check if size is OK + fi, err := f.Stat() + if err != nil { + ui.Error("Image stat failed: " + err.Error()) + state.Put("error", err) + return multistep.ActionHalt + } + + if fi.Size() != imageToExport.size { + os.Remove(tempDestinationPath) + ui.Error("image size mistmatch") + state.Put("error", fmt.Errorf("image size mistmatch")) + return multistep.ActionHalt + } + + name = name + ".img" + os.Rename(tempDestinationPath, name) + + ui.Message(fmt.Sprintf("image %s exported", name)) + + case <-sigChan: + // We received a signal, cancel the copy operation + toRead.Close() + f.Close() + os.Remove(tempDestinationPath) + ui.Message("image export cancelled") + return multistep.ActionHalt + } + + } + return multistep.ActionContinue +} + +func (s *stepExportImage) Cleanup(state multistep.StateBag) {} diff --git a/docs/builders/nutanix.mdx b/docs/builders/nutanix.mdx index 431fbbb..05dd405 100644 --- a/docs/builders/nutanix.mdx +++ b/docs/builders/nutanix.mdx @@ -42,7 +42,8 @@ These parameters allow to configure everything around image creation, from the t - `image_description` (string) - Description for output image. - `image_categories` ([]Category) - Assign Categories to the image. - `force_deregister` (bool) - Allow output image override if already exists. -- `image_delete` (bool) - Delete image once build process is completed. +- `image_delete` (bool) - Delete image once build process is completed (default is false). +- `image_export` (bool) - Export raw image in the current folder (default is false). - `shutdown_command` (string) - Command line to shutdown your temporary VM. - `shutdown_timeout` (string) - Timeout for VM shutdown (format : 2m). - `communicator` (string) - Protocol used for Packer connection (ex "winrm" or "ssh"). Default is : "ssh". diff --git a/go.mod b/go.mod index 1ccafce..69f2ca4 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,7 @@ replace ( require ( github.com/hashicorp/hcl/v2 v2.14.1 - github.com/hashicorp/packer-plugin-sdk v0.3.2 + github.com/hashicorp/packer-plugin-sdk v0.4.0 github.com/nutanix-cloud-native/prism-go-client v0.2.0 github.com/zclconf/go-cty v1.10.0 ) @@ -78,7 +78,7 @@ require ( github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786 // indirect github.com/masterzen/winrm v0.0.0-20210623064412-3b76017826b0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect - github.com/mattn/go-isatty v0.0.13 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-fs v0.0.0-20180402235330-b7b9ca407fff // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect @@ -95,11 +95,11 @@ require ( github.com/ulikunitz/xz v0.5.10 // indirect go.opencensus.io v0.23.0 // indirect golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 // indirect - golang.org/x/net v0.7.0 // indirect + golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.1.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.101.0 // indirect diff --git a/go.sum b/go.sum index 454b6c0..9241164 100644 --- a/go.sum +++ b/go.sum @@ -291,8 +291,8 @@ github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= github.com/hashicorp/memberlist v0.2.4 h1:OOhYzSvFnkFQXm1ysE8RjXTHsqSRDyP4emusC9K7DYg= github.com/hashicorp/memberlist v0.2.4/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= -github.com/hashicorp/packer-plugin-sdk v0.3.2 h1:4Kqq7B8CRDMbfZmkloyz11t1hfqazJuBbW8ZFo4QlN4= -github.com/hashicorp/packer-plugin-sdk v0.3.2/go.mod h1:XZRvL9kRqJJtB6rf9Lu2zWLJbf2/4ImWXDjp9O9UQGE= +github.com/hashicorp/packer-plugin-sdk v0.4.0 h1:UyLYe0y02D9wkOQ3FeeZWyFg2+mx2vLuWRGUL5xt50I= +github.com/hashicorp/packer-plugin-sdk v0.4.0/go.mod h1:uNhU3pmjM2ejgHYce/g4J+sa5rh81iYQztpGvGa5FOs= github.com/hashicorp/serf v0.9.5 h1:EBWvyu9tcRszt3Bxp3KNssBMP1KuHWyO51lz9+786iM= github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/terraform-exec v0.16.1/go.mod h1:aj0lVshy8l+MHhFNoijNHtqTJQI3Xlowv5EOsEaGO7M= @@ -367,8 +367,8 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= -github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= +github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= @@ -571,8 +571,8 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -625,19 +625,20 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=