mirror of
https://github.com/bpg/terraform-provider-proxmox.git
synced 2025-06-29 18:21:10 +00:00
Signed-off-by: Marco Attia <54147992+Vaneixus@users.noreply.github.com> Signed-off-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com> Co-authored-by: Pavel Boldyrev <627562+bpg@users.noreply.github.com>
965 lines
29 KiB
Go
965 lines
29 KiB
Go
/*
|
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
*/
|
|
|
|
package resource
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-cty/cty"
|
|
"github.com/hashicorp/terraform-plugin-log/tflog"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
|
|
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
|
|
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/api"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmox/version"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmoxtf"
|
|
"github.com/bpg/terraform-provider-proxmox/proxmoxtf/resource/validators"
|
|
"github.com/bpg/terraform-provider-proxmox/utils"
|
|
)
|
|
|
|
const (
|
|
dvResourceVirtualEnvironmentFileSourceFileChanged = false
|
|
dvResourceVirtualEnvironmentFileSourceFileChecksum = ""
|
|
dvResourceVirtualEnvironmentFileSourceFileFileName = ""
|
|
dvResourceVirtualEnvironmentFileSourceFileInsecure = false
|
|
dvResourceVirtualEnvironmentFileSourceFileMinTLS = ""
|
|
dvResourceVirtualEnvironmentFileOverwrite = true
|
|
dvResourceVirtualEnvironmentFileSourceRawResize = 0
|
|
dvResourceVirtualEnvironmentFileTimeoutUpload = 1800
|
|
|
|
mkResourceVirtualEnvironmentFileContentType = "content_type"
|
|
mkResourceVirtualEnvironmentFileDatastoreID = "datastore_id"
|
|
mkResourceVirtualEnvironmentFileFileModificationDate = "file_modification_date"
|
|
mkResourceVirtualEnvironmentFileFileName = "file_name"
|
|
mkResourceVirtualEnvironmentFileFileMode = "file_mode"
|
|
mkResourceVirtualEnvironmentFileFileSize = "file_size"
|
|
mkResourceVirtualEnvironmentFileFileTag = "file_tag"
|
|
mkResourceVirtualEnvironmentFileNodeName = "node_name"
|
|
mkResourceVirtualEnvironmentFileOverwrite = "overwrite"
|
|
mkResourceVirtualEnvironmentFileSourceFile = "source_file"
|
|
mkResourceVirtualEnvironmentFileSourceFilePath = "path"
|
|
mkResourceVirtualEnvironmentFileSourceFileChanged = "changed"
|
|
mkResourceVirtualEnvironmentFileSourceFileChecksum = "checksum"
|
|
mkResourceVirtualEnvironmentFileSourceFileFileName = "file_name"
|
|
mkResourceVirtualEnvironmentFileSourceFileInsecure = "insecure"
|
|
mkResourceVirtualEnvironmentFileSourceFileMinTLS = "min_tls"
|
|
mkResourceVirtualEnvironmentFileSourceRaw = "source_raw"
|
|
mkResourceVirtualEnvironmentFileSourceRawData = "data"
|
|
mkResourceVirtualEnvironmentFileSourceRawFileName = "file_name"
|
|
mkResourceVirtualEnvironmentFileSourceRawResize = "resize"
|
|
mkResourceVirtualEnvironmentFileTimeoutUpload = "timeout_upload"
|
|
)
|
|
|
|
// File returns a resource that manages files on a node.
|
|
func File() *schema.Resource {
|
|
return &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
mkResourceVirtualEnvironmentFileContentType: {
|
|
Type: schema.TypeString,
|
|
Description: "The content type",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Computed: true,
|
|
ValidateDiagFunc: validators.ContentType(),
|
|
},
|
|
mkResourceVirtualEnvironmentFileDatastoreID: {
|
|
Type: schema.TypeString,
|
|
Description: "The datastore id",
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileFileModificationDate: {
|
|
Type: schema.TypeString,
|
|
Description: "The file modification date",
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileFileName: {
|
|
Type: schema.TypeString,
|
|
Description: "The file name",
|
|
Computed: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileFileMode: {
|
|
Type: schema.TypeString,
|
|
Description: `The file mode in octal format, e.g. "0700" or "600".` +
|
|
`Note that the prefixes "0o" and "0x" are not supported!` +
|
|
`Setting this attribute is also only allowed for "root@pam" authenticated user.`,
|
|
Optional: true,
|
|
ValidateDiagFunc: validators.FileMode(),
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileFileSize: {
|
|
Type: schema.TypeInt,
|
|
Description: "The file size in bytes",
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileFileTag: {
|
|
Type: schema.TypeString,
|
|
Description: "The file tag",
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileNodeName: {
|
|
Type: schema.TypeString,
|
|
Description: "The node name",
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceFile: {
|
|
Type: schema.TypeList,
|
|
Description: "The source file",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
DefaultFunc: func() (interface{}, error) {
|
|
return make([]interface{}, 1), nil
|
|
},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
mkResourceVirtualEnvironmentFileSourceFilePath: {
|
|
Type: schema.TypeString,
|
|
Description: "A path to a local file or a URL",
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceFileChanged: {
|
|
Type: schema.TypeBool,
|
|
Description: "Whether the source file has changed since the last run",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: dvResourceVirtualEnvironmentFileSourceFileChanged,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceFileChecksum: {
|
|
Type: schema.TypeString,
|
|
Description: "The SHA256 checksum of the source file",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: dvResourceVirtualEnvironmentFileSourceFileChecksum,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceFileFileName: {
|
|
Type: schema.TypeString,
|
|
Description: "The file name to use instead of the source file name",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: dvResourceVirtualEnvironmentFileSourceFileFileName,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceFileInsecure: {
|
|
Type: schema.TypeBool,
|
|
Description: "Whether to skip the TLS verification step for HTTPS sources",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: dvResourceVirtualEnvironmentFileSourceFileInsecure,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceFileMinTLS: {
|
|
Type: schema.TypeString,
|
|
Description: "The minimum required TLS version for HTTPS sources." +
|
|
"Supported values: `1.0|1.1|1.2|1.3`. Defaults to `1.3`.",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: dvResourceVirtualEnvironmentFileSourceFileMinTLS,
|
|
},
|
|
},
|
|
},
|
|
MaxItems: 1,
|
|
MinItems: 0,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceRaw: {
|
|
Type: schema.TypeList,
|
|
Description: "The raw source",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
DefaultFunc: func() (interface{}, error) {
|
|
return make([]interface{}, 1), nil
|
|
},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
mkResourceVirtualEnvironmentFileSourceRawData: {
|
|
Type: schema.TypeString,
|
|
Description: "The raw data",
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceRawFileName: {
|
|
Type: schema.TypeString,
|
|
Description: "The file name",
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
mkResourceVirtualEnvironmentFileSourceRawResize: {
|
|
Type: schema.TypeInt,
|
|
Description: "The number of bytes to resize the file to",
|
|
Optional: true,
|
|
ForceNew: true,
|
|
Default: dvResourceVirtualEnvironmentFileSourceRawResize,
|
|
},
|
|
},
|
|
},
|
|
MaxItems: 1,
|
|
MinItems: 0,
|
|
},
|
|
mkResourceVirtualEnvironmentFileTimeoutUpload: {
|
|
Type: schema.TypeInt,
|
|
Description: "Timeout for uploading ISO/VSTMPL files in seconds",
|
|
Optional: true,
|
|
Default: dvResourceVirtualEnvironmentFileTimeoutUpload,
|
|
},
|
|
mkResourceVirtualEnvironmentFileOverwrite: {
|
|
Type: schema.TypeBool,
|
|
Description: "Whether to overwrite the file if it already exists",
|
|
Optional: true,
|
|
Default: dvResourceVirtualEnvironmentFileOverwrite,
|
|
},
|
|
},
|
|
CreateContext: fileCreate,
|
|
ReadContext: fileRead,
|
|
DeleteContext: fileDelete,
|
|
UpdateContext: fileUpdate,
|
|
Importer: &schema.ResourceImporter{
|
|
StateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) ([]*schema.ResourceData, error) {
|
|
node, volID, err := fileParseImportID(d.Id())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
d.SetId(volID.String())
|
|
|
|
err = d.Set(mkResourceVirtualEnvironmentFileNodeName, node)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed setting 'node_name' in state during import: %w", err)
|
|
}
|
|
|
|
err = d.Set(mkResourceVirtualEnvironmentFileDatastoreID, volID.datastoreID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed setting 'datastore_id' in state during import: %w", err)
|
|
}
|
|
|
|
err = d.Set(mkResourceVirtualEnvironmentFileContentType, volID.contentType)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed setting 'content_type' in state during import: %w", err)
|
|
}
|
|
|
|
return []*schema.ResourceData{d}, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
type fileVolumeID struct {
|
|
datastoreID string
|
|
contentType string
|
|
fileName string
|
|
}
|
|
|
|
func (v fileVolumeID) String() string {
|
|
return fmt.Sprintf("%s:%s/%s", v.datastoreID, v.contentType, v.fileName)
|
|
}
|
|
|
|
// fileParseVolumeID parses a volume ID in the format datastore_id:content_type/file_name.
|
|
func fileParseVolumeID(id string) (fileVolumeID, error) {
|
|
parts := strings.SplitN(id, ":", 2)
|
|
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return fileVolumeID{}, fmt.Errorf("unexpected format of ID (%s), expected datastore_id:content_type/file_name", id)
|
|
}
|
|
|
|
datastoreID := parts[0]
|
|
|
|
parts = strings.SplitN(parts[1], "/", 2)
|
|
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return fileVolumeID{}, fmt.Errorf("unexpected format of ID (%s), expected datastore_id:content_type/file_name", id)
|
|
}
|
|
|
|
contentType := parts[0]
|
|
fileName := parts[1]
|
|
|
|
return fileVolumeID{
|
|
datastoreID: datastoreID,
|
|
contentType: contentType,
|
|
fileName: fileName,
|
|
}, nil
|
|
}
|
|
|
|
// fileParseImportID parses an import ID in the format node/datastore_id:content_type/file_name.
|
|
func fileParseImportID(id string) (string, fileVolumeID, error) {
|
|
parts := strings.SplitN(id, "/", 2)
|
|
|
|
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
|
|
return "", fileVolumeID{},
|
|
fmt.Errorf("unexpected format of ID (%s), expected node/datastore_id:content_type/file_name", id)
|
|
}
|
|
|
|
node := parts[0]
|
|
|
|
volID, err := fileParseVolumeID(parts[1])
|
|
if err != nil {
|
|
return "", fileVolumeID{}, err
|
|
}
|
|
|
|
return node, volID, nil
|
|
}
|
|
|
|
func fileCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
|
|
uploadTimeout := d.Get(mkResourceVirtualEnvironmentFileTimeoutUpload).(int)
|
|
fileMode := d.Get(mkResourceVirtualEnvironmentFileFileMode).(string)
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(uploadTimeout)*time.Second)
|
|
defer cancel()
|
|
|
|
var diags diag.Diagnostics
|
|
|
|
fileName, err := fileGetSourceFileName(d)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
|
|
datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
|
|
|
|
config := m.(proxmoxtf.ProviderConfiguration)
|
|
|
|
capi, err := config.GetClient()
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
contentType, dg := fileGetContentType(ctx, d, capi)
|
|
diags = append(diags, dg...)
|
|
|
|
list, err := capi.Node(nodeName).Storage(datastoreID).ListDatastoreFiles(ctx)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
for _, file := range list {
|
|
volumeID, e := fileParseVolumeID(file.VolumeID)
|
|
if e != nil {
|
|
tflog.Warn(ctx, "failed to parse volume ID", map[string]interface{}{
|
|
"error": err,
|
|
})
|
|
|
|
continue
|
|
}
|
|
|
|
if volumeID.fileName == *fileName {
|
|
if d.Get(mkResourceVirtualEnvironmentFileOverwrite).(bool) {
|
|
diags = append(diags, diag.Diagnostic{
|
|
Severity: diag.Warning,
|
|
Summary: fmt.Sprintf("the existing file %q has been overwritten by the resource", volumeID),
|
|
})
|
|
} else {
|
|
return diag.Errorf("file %q already exists", volumeID)
|
|
}
|
|
}
|
|
}
|
|
|
|
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
|
|
sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
|
|
|
|
sourceFilePathLocal := ""
|
|
|
|
// Determine if both source_data and source_file is specified as this is not supported.
|
|
if len(sourceFile) > 0 && len(sourceRaw) > 0 {
|
|
diags = append(diags, diag.Errorf(
|
|
"please specify \"%s.%s\" or \"%s\" - not both",
|
|
mkResourceVirtualEnvironmentFileSourceFile,
|
|
mkResourceVirtualEnvironmentFileSourceFilePath,
|
|
mkResourceVirtualEnvironmentFileSourceRaw,
|
|
)...)
|
|
}
|
|
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
// Determine if we're dealing with raw file data or a reference to a file or URL.
|
|
// In case of a URL, we must first download the file before proceeding.
|
|
// This is due to lack of support for chunked transfers in the Proxmox VE API.
|
|
if len(sourceFile) > 0 {
|
|
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
|
sourceFilePath := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
|
sourceFileChecksum := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileChecksum].(string)
|
|
sourceFileMinTLS := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileMinTLS].(string)
|
|
sourceFileInsecure := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileInsecure].(bool)
|
|
|
|
if fileIsURL(d) {
|
|
tflog.Debug(ctx, "Downloading file from URL", map[string]interface{}{
|
|
"url": sourceFilePath,
|
|
})
|
|
|
|
minTLSVersion, e := api.GetMinTLSVersion(sourceFileMinTLS)
|
|
if e != nil {
|
|
return diag.FromErr(e)
|
|
}
|
|
|
|
httpClient := http.Client{
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{
|
|
MinVersion: minTLSVersion,
|
|
InsecureSkipVerify: sourceFileInsecure,
|
|
},
|
|
},
|
|
}
|
|
|
|
res, err := httpClient.Get(sourceFilePath)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
defer utils.CloseOrLogError(ctx)(res.Body)
|
|
|
|
tempDownloadedFile, err := os.CreateTemp(config.TempDir(), "download")
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
tempDownloadedFileName := tempDownloadedFile.Name()
|
|
defer func(name string) {
|
|
err := os.Remove(name)
|
|
if err != nil {
|
|
tflog.Error(ctx, "Failed to remove temporary file", map[string]interface{}{
|
|
"error": err,
|
|
"file": name,
|
|
})
|
|
}
|
|
}(tempDownloadedFileName)
|
|
|
|
_, err = io.Copy(tempDownloadedFile, res.Body)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
err = tempDownloadedFile.Close()
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
sourceFilePathLocal = tempDownloadedFileName
|
|
} else {
|
|
sourceFilePathLocal = sourceFilePath
|
|
}
|
|
|
|
// Calculate the checksum of the source file now that it's available locally.
|
|
if sourceFileChecksum != "" {
|
|
file, err := os.Open(sourceFilePathLocal)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
h := sha256.New()
|
|
_, err = io.Copy(h, file)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
err = file.Close()
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
calculatedChecksum := fmt.Sprintf("%x", h.Sum(nil))
|
|
tflog.Debug(ctx, "Calculated checksum", map[string]interface{}{
|
|
"source": sourceFilePath,
|
|
"sha256": calculatedChecksum,
|
|
})
|
|
|
|
if sourceFileChecksum != calculatedChecksum {
|
|
return diag.Errorf(
|
|
"the calculated SHA256 checksum \"%s\" does not match source checksum \"%s\"",
|
|
calculatedChecksum,
|
|
sourceFileChecksum,
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
//nolint:nestif
|
|
if len(sourceRaw) > 0 {
|
|
sourceRawBlock := sourceRaw[0].(map[string]interface{})
|
|
sourceRawData := sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawData].(string)
|
|
sourceRawResize := sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawResize].(int)
|
|
|
|
if sourceRawResize > 0 {
|
|
if len(sourceRawData) <= sourceRawResize {
|
|
sourceRawData = fmt.Sprintf(fmt.Sprintf("%%-%dv", sourceRawResize), sourceRawData)
|
|
} else {
|
|
return diag.Errorf("cannot resize %d bytes to %d bytes", len(sourceRawData), sourceRawResize)
|
|
}
|
|
}
|
|
|
|
tempRawFile, e := os.CreateTemp(config.TempDir(), "raw")
|
|
if e != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
tempRawFileName := tempRawFile.Name()
|
|
_, err = io.Copy(tempRawFile, bytes.NewBufferString(sourceRawData))
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
err = tempRawFile.Close()
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
defer func(name string) {
|
|
err := os.Remove(name)
|
|
if err != nil {
|
|
tflog.Error(ctx, "Failed to remove temporary file", map[string]interface{}{
|
|
"error": err,
|
|
"file": name,
|
|
})
|
|
}
|
|
}(tempRawFileName)
|
|
|
|
sourceFilePathLocal = tempRawFileName
|
|
}
|
|
|
|
// Open the source file for reading in order to upload it.
|
|
file, err := os.Open(sourceFilePathLocal)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
defer func(file *os.File) {
|
|
err := file.Close()
|
|
if err != nil {
|
|
tflog.Error(ctx, "Failed to close file", map[string]interface{}{
|
|
"error": err,
|
|
})
|
|
}
|
|
}(file)
|
|
|
|
request := &api.FileUploadRequest{
|
|
ContentType: *contentType,
|
|
FileName: *fileName,
|
|
File: file,
|
|
Mode: fileMode,
|
|
}
|
|
|
|
switch *contentType {
|
|
case "iso", "vztmpl", "import":
|
|
_, err = capi.Node(nodeName).Storage(datastoreID).APIUpload(
|
|
ctx, request, config.TempDir(),
|
|
)
|
|
if err != nil {
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
return diags
|
|
}
|
|
default:
|
|
// For all other content types, we need to upload the file to the node's
|
|
// datastore using SFTP.
|
|
datastore, err2 := capi.Storage().GetDatastore(ctx, datastoreID)
|
|
if err2 != nil {
|
|
return diag.Errorf("failed to get datastore: %s", err2)
|
|
}
|
|
|
|
if datastore.Path == nil || *datastore.Path == "" {
|
|
return diag.Errorf("failed to determine the datastore path")
|
|
}
|
|
|
|
sort.Strings(datastore.Content)
|
|
|
|
_, found := slices.BinarySearch(datastore.Content, *contentType)
|
|
if !found {
|
|
diags = append(diags, diag.Diagnostics{
|
|
diag.Diagnostic{
|
|
Severity: diag.Warning,
|
|
Summary: fmt.Sprintf("the datastore %q does not support content type %q; supported content types are: %v",
|
|
*datastore.Storage, *contentType, datastore.Content,
|
|
),
|
|
},
|
|
}...)
|
|
}
|
|
|
|
// PVE expects backups to be located at the "dump" directory of the datastore.
|
|
if *contentType == "backup" {
|
|
request.ContentType = "dump"
|
|
}
|
|
|
|
err = capi.SSH().NodeStreamUpload(ctx, nodeName, *datastore.Path, request)
|
|
if err != nil {
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
return diags
|
|
}
|
|
|
|
}
|
|
|
|
volID, di := fileGetVolumeID(ctx, d, capi)
|
|
diags = append(diags, di...)
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
|
|
d.SetId(volID.String())
|
|
|
|
diags = append(diags, fileRead(ctx, d, m)...)
|
|
|
|
if d.Id() == "" {
|
|
diags = append(diags, diag.Errorf("failed to read file from %q", volID.String())...)
|
|
}
|
|
|
|
return diags
|
|
}
|
|
|
|
func fileGetContentType(ctx context.Context, d *schema.ResourceData, c proxmox.Client) (*string, diag.Diagnostics) {
|
|
contentType := d.Get(mkResourceVirtualEnvironmentFileContentType).(string)
|
|
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
|
|
sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
|
|
|
|
ver := version.MinimumProxmoxVersion
|
|
if versionResp, err := c.Version().Version(ctx); err == nil {
|
|
ver = versionResp.Version
|
|
} else {
|
|
tflog.Warn(ctx, fmt.Sprintf("failed to determine Proxmox VE version, assume %v", ver), map[string]interface{}{
|
|
"error": err,
|
|
})
|
|
}
|
|
|
|
sourceFilePath := ""
|
|
|
|
if len(sourceFile) > 0 {
|
|
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
|
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
|
} else if len(sourceRaw) > 0 {
|
|
sourceRawBlock := sourceRaw[0].(map[string]interface{})
|
|
sourceFilePath = sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawFileName].(string)
|
|
} else {
|
|
return nil, diag.Errorf(
|
|
"missing argument \"%s.%s\" or \"%s\"",
|
|
mkResourceVirtualEnvironmentFileSourceFile,
|
|
mkResourceVirtualEnvironmentFileSourceFilePath,
|
|
mkResourceVirtualEnvironmentFileSourceRaw,
|
|
)
|
|
}
|
|
if contentType == "" {
|
|
if strings.HasSuffix(sourceFilePath, ".tar.gz") ||
|
|
strings.HasSuffix(sourceFilePath, ".tar.xz") {
|
|
contentType = "vztmpl"
|
|
} else if ver.SupportImportContentType() &&
|
|
(strings.HasSuffix(sourceFilePath, ".qcow2") ||
|
|
strings.HasSuffix(sourceFilePath, ".raw") ||
|
|
strings.HasSuffix(sourceFilePath, ".vmdk")) {
|
|
contentType = "import"
|
|
} else {
|
|
ext := strings.TrimLeft(strings.ToLower(filepath.Ext(sourceFilePath)), ".")
|
|
|
|
switch ext {
|
|
case "img", "iso":
|
|
contentType = "iso"
|
|
case "yaml", "yml":
|
|
contentType = "snippets"
|
|
}
|
|
}
|
|
|
|
if contentType == "" {
|
|
return nil, diag.Errorf(
|
|
"cannot determine the content type of source \"%s\" - Please manually define the \"%s\" argument",
|
|
sourceFilePath,
|
|
mkResourceVirtualEnvironmentFileContentType,
|
|
)
|
|
}
|
|
}
|
|
|
|
ctValidator := validators.ContentType()
|
|
diags := ctValidator(contentType, cty.GetAttrPath(mkResourceVirtualEnvironmentFileContentType))
|
|
|
|
return &contentType, diags
|
|
}
|
|
|
|
func fileGetSourceFileName(d *schema.ResourceData) (*string, error) {
|
|
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
|
|
sourceRaw := d.Get(mkResourceVirtualEnvironmentFileSourceRaw).([]interface{})
|
|
|
|
sourceFileFileName := ""
|
|
sourceFilePath := ""
|
|
|
|
if len(sourceFile) > 0 {
|
|
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
|
sourceFileFileName = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileFileName].(string)
|
|
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
|
} else if len(sourceRaw) > 0 {
|
|
sourceRawBlock := sourceRaw[0].(map[string]interface{})
|
|
sourceFileFileName = sourceRawBlock[mkResourceVirtualEnvironmentFileSourceRawFileName].(string)
|
|
} else {
|
|
return nil, fmt.Errorf(
|
|
"missing argument \"%s.%s\"",
|
|
mkResourceVirtualEnvironmentFileSourceFile,
|
|
mkResourceVirtualEnvironmentFileSourceFilePath,
|
|
)
|
|
}
|
|
|
|
if sourceFileFileName == "" {
|
|
if fileIsURL(d) {
|
|
downloadURL, err := url.ParseRequestURI(sourceFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
path := strings.Split(downloadURL.Path, "/")
|
|
sourceFileFileName = path[len(path)-1]
|
|
|
|
if sourceFileFileName == "" {
|
|
return nil, fmt.Errorf(
|
|
"failed to determine file name from the URL \"%s\"",
|
|
sourceFilePath,
|
|
)
|
|
}
|
|
} else {
|
|
sourceFileFileName = filepath.Base(sourceFilePath)
|
|
}
|
|
}
|
|
|
|
return &sourceFileFileName, nil
|
|
}
|
|
|
|
func fileGetVolumeID(ctx context.Context, d *schema.ResourceData, c proxmox.Client) (fileVolumeID, diag.Diagnostics) {
|
|
fileName, err := fileGetSourceFileName(d)
|
|
if err != nil {
|
|
return fileVolumeID{}, diag.FromErr(err)
|
|
}
|
|
|
|
datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
|
|
contentType, diags := fileGetContentType(ctx, d, c)
|
|
|
|
return fileVolumeID{
|
|
datastoreID: datastoreID,
|
|
contentType: *contentType,
|
|
fileName: *fileName,
|
|
}, diags
|
|
}
|
|
|
|
func fileIsURL(d *schema.ResourceData) bool {
|
|
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
|
|
sourceFilePath := ""
|
|
|
|
if len(sourceFile) > 0 {
|
|
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
|
sourceFilePath = sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
|
} else {
|
|
return false
|
|
}
|
|
|
|
return strings.HasPrefix(sourceFilePath, "http://") ||
|
|
strings.HasPrefix(sourceFilePath, "https://")
|
|
}
|
|
|
|
func fileRead(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
|
|
config := m.(proxmoxtf.ProviderConfiguration)
|
|
capi, err := config.GetClient()
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
|
|
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
|
|
sourceFile := d.Get(mkResourceVirtualEnvironmentFileSourceFile).([]interface{})
|
|
|
|
list, err := capi.Node(nodeName).Storage(datastoreID).ListDatastoreFiles(ctx)
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
readFileAttrs := readFile
|
|
if fileIsURL(d) {
|
|
readFileAttrs = readURL(capi.API().HTTP())
|
|
}
|
|
|
|
var diags diag.Diagnostics
|
|
|
|
found := false
|
|
for _, v := range list {
|
|
if v.VolumeID == d.Id() {
|
|
found = true
|
|
|
|
volID, err := fileParseVolumeID(v.VolumeID)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
err = d.Set(mkResourceVirtualEnvironmentFileFileName, volID.fileName)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
err = d.Set(mkResourceVirtualEnvironmentFileContentType, v.ContentType)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
if len(sourceFile) == 0 {
|
|
continue
|
|
}
|
|
|
|
sourceFileBlock := sourceFile[0].(map[string]interface{})
|
|
sourceFilePath := sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFilePath].(string)
|
|
|
|
fileModificationDate, fileSize, fileTag, err := readFileAttrs(ctx, sourceFilePath)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
if fileModificationDate != "" || fileSize != 0 || fileTag != "" {
|
|
// only when file from state exists
|
|
err = d.Set(mkResourceVirtualEnvironmentFileFileModificationDate, fileModificationDate)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
err = d.Set(mkResourceVirtualEnvironmentFileFileSize, fileSize)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
err = d.Set(mkResourceVirtualEnvironmentFileFileTag, fileTag)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
}
|
|
|
|
lastFileMD := d.Get(mkResourceVirtualEnvironmentFileFileModificationDate).(string)
|
|
lastFileSize := int64(d.Get(mkResourceVirtualEnvironmentFileFileSize).(int))
|
|
lastFileTag := d.Get(mkResourceVirtualEnvironmentFileFileTag).(string)
|
|
|
|
// just to make the logic easier to read
|
|
changed := false
|
|
if lastFileMD != "" && lastFileSize != 0 && lastFileTag != "" {
|
|
changed = lastFileMD != fileModificationDate || lastFileSize != fileSize || lastFileTag != fileTag
|
|
}
|
|
|
|
sourceFileBlock[mkResourceVirtualEnvironmentFileSourceFileChanged] = changed
|
|
err = d.Set(mkResourceVirtualEnvironmentFileSourceFile, sourceFile)
|
|
diags = append(diags, diag.FromErr(err)...)
|
|
|
|
if diags.HasError() {
|
|
return diags
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
// an empty ID is used to signal that the resource does not exist when provider reads the state
|
|
// back after creation, or on the state refresh.
|
|
d.SetId("")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
//nolint:nonamedreturns
|
|
func readFile(
|
|
ctx context.Context,
|
|
sourceFilePath string,
|
|
) (fileModificationDate string, fileSize int64, fileTag string, err error) {
|
|
f, err := os.Open(sourceFilePath)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// File does not exist, return zero values and no error
|
|
return "", 0, "", nil
|
|
}
|
|
return
|
|
}
|
|
|
|
defer func(f *os.File) {
|
|
e := f.Close()
|
|
if e != nil {
|
|
tflog.Error(ctx, "failed to close the file", map[string]interface{}{
|
|
"error": e.Error(),
|
|
})
|
|
}
|
|
}(f)
|
|
|
|
fileInfo, err := f.Stat()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
fileModificationDate = fileInfo.ModTime().UTC().Format(time.RFC3339)
|
|
fileSize = fileInfo.Size()
|
|
fileTag = fmt.Sprintf("%x-%x", fileInfo.ModTime().UTC().Unix(), fileInfo.Size())
|
|
|
|
return fileModificationDate, fileSize, fileTag, nil
|
|
}
|
|
|
|
func readURL(
|
|
httClient *http.Client,
|
|
) func(
|
|
ctx context.Context,
|
|
sourceFilePath string,
|
|
) (fileModificationDate string, fileSize int64, fileTag string, err error) {
|
|
return func(
|
|
ctx context.Context,
|
|
sourceFilePath string,
|
|
) (string, int64, string, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodHead, sourceFilePath, nil)
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("failed to create a new request: %w", err)
|
|
}
|
|
|
|
res, err := httClient.Do(req) //nolint:bodyclose
|
|
if err != nil {
|
|
return "", 0, "", fmt.Errorf("failed to HEAD the URL: %w", err)
|
|
}
|
|
|
|
defer utils.CloseOrLogError(ctx)(res.Body)
|
|
|
|
fileModificationDate := ""
|
|
fileSize := res.ContentLength
|
|
fileTag := ""
|
|
httpLastModified := res.Header.Get("Last-Modified")
|
|
|
|
if httpLastModified != "" {
|
|
var timeParsed time.Time
|
|
timeParsed, err = time.Parse(time.RFC1123, httpLastModified)
|
|
if err != nil {
|
|
timeParsed, err = time.Parse(time.RFC1123Z, httpLastModified)
|
|
if err != nil {
|
|
return fileModificationDate, fileSize, fileTag, fmt.Errorf("failed to parse Last-Modified header: %w", err)
|
|
}
|
|
}
|
|
|
|
fileModificationDate = timeParsed.UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
httpTag := res.Header.Get("ETag")
|
|
|
|
if httpTag != "" {
|
|
httpTagParts := strings.Split(httpTag, "\"")
|
|
|
|
if len(httpTagParts) > 1 {
|
|
fileTag = httpTagParts[1]
|
|
}
|
|
}
|
|
|
|
return fileModificationDate, fileSize, fileTag, nil
|
|
}
|
|
}
|
|
|
|
func fileDelete(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics {
|
|
config := m.(proxmoxtf.ProviderConfiguration)
|
|
capi, err := config.GetClient()
|
|
if err != nil {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
datastoreID := d.Get(mkResourceVirtualEnvironmentFileDatastoreID).(string)
|
|
nodeName := d.Get(mkResourceVirtualEnvironmentFileNodeName).(string)
|
|
|
|
err = capi.Node(nodeName).Storage(datastoreID).DeleteDatastoreFile(ctx, d.Id())
|
|
if err != nil && !errors.Is(err, api.ErrResourceDoesNotExist) {
|
|
return diag.FromErr(err)
|
|
}
|
|
|
|
d.SetId("")
|
|
|
|
return nil
|
|
}
|
|
|
|
func fileUpdate(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics {
|
|
// a pass-through update function -- no actual resource update is needed / allowed
|
|
// only the TF state is updated, for example, a timeout_upload attribute value
|
|
return nil
|
|
}
|