//go:build windows
// +build windows

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.

package credentialspec

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"
	"time"

	asmfactory "github.com/aws/amazon-ecs-agent/agent/asm/factory"
	"github.com/aws/amazon-ecs-agent/agent/s3"
	s3factory "github.com/aws/amazon-ecs-agent/agent/s3/factory"
	"github.com/aws/amazon-ecs-agent/agent/ssm"
	ssmfactory "github.com/aws/amazon-ecs-agent/agent/ssm/factory"
	"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
	"github.com/aws/amazon-ecs-agent/agent/utils/oswrapper"
	"github.com/aws/amazon-ecs-agent/ecs-agent/credentials"
	"github.com/aws/amazon-ecs-agent/ecs-agent/ipcompatibility"
	internalarn "github.com/aws/amazon-ecs-agent/ecs-agent/utils/arn"

	"github.com/aws/aws-sdk-go-v2/aws/arn"
	"github.com/cihub/seelog"
	"github.com/pkg/errors"
	"golang.org/x/sys/windows/registry"
)

const (
	tempFileName = "temp_file"
	// filePerm is the permission for the credentialspec file.
	filePerm = 0644

	s3DownloadTimeout = 30 * time.Second

	// Environment variables to setup resource location
	envProgramData              = "ProgramData"
	dockerCredentialSpecDataDir = "docker/credentialspecs"
	ecsCcgPluginRegistryKeyRoot = `System\CurrentControlSet\Services\AmazonECSCCGPlugin`
	regKeyPathFormat            = `HKEY_LOCAL_MACHINE\` + ecsCcgPluginRegistryKeyRoot + `\%s`

	credentialSpecParseErrorMsgTemplate = "Unable to parse %s from credential spec"
	untypedMarshallErrorMsgTemplate     = "Unable to marshal untyped object %s to type %s"
)

var (
	// For ease of unit testing
	osWriteFileImpl                           = os.WriteFile
	osReadFileImpl                            = os.ReadFile
	osRemoveImpl                              = os.Remove
	readCredentialSpecImpl                    = readCredentialSpec
	writeCredentialSpecImpl                   = writeCredentialSpec
	readWriteDomainlessCredentialSpecImpl     = readWriteDomainlessCredentialSpec
	setTaskExecutionCredentialsRegKeysImpl    = SetTaskExecutionCredentialsRegKeys
	handleNonFileDomainlessGMSACredSpecImpl   = handleNonFileDomainlessGMSACredSpec
	deleteTaskExecutionCredentialsRegKeysImpl = deleteTaskExecutionCredentialsRegKeys
)

type pluginInput struct {
	CredentialArn string `json:"credentialArn,omitempty"`
	RegKeyPath    string `json:"regKeyPath,omitempty"`
}

// CredentialSpecResource is the abstraction for credentialspec resources
type CredentialSpecResource struct {
	*CredentialSpecResourceCommon
	ioutil ioutilwrapper.IOUtil
	// credentialSpecResourceLocation is the location for all the tasks' credentialspec artifacts
	credentialSpecResourceLocation string
	isDomainlessGMSATask           bool
}

// NewCredentialSpecResource creates a new CredentialSpecResource object
func NewCredentialSpecResource(taskARN, region string,
	executionCredentialsID string,
	credentialsManager credentials.Manager,
	ssmClientCreator ssmfactory.SSMClientCreator,
	s3ClientCreator s3factory.S3ClientCreator,
	asmClientCreator asmfactory.ClientCreator,
	credentialSpecContainerMap map[string]string,
	ipCompatibility ipcompatibility.IPCompatibility) (*CredentialSpecResource, error) {

	s := &CredentialSpecResource{
		CredentialSpecResourceCommon: &CredentialSpecResourceCommon{
			taskARN:                    taskARN,
			region:                     region,
			credentialsManager:         credentialsManager,
			executionCredentialsID:     executionCredentialsID,
			ssmClientCreator:           ssmClientCreator,
			s3ClientCreator:            s3ClientCreator,
			CredSpecMap:                make(map[string]string),
			credentialSpecContainerMap: credentialSpecContainerMap,
			ipCompatibility:            ipCompatibility,
		},
		ioutil:               ioutilwrapper.NewIOUtil(),
		isDomainlessGMSATask: false,
	}

	err := s.setCredentialSpecResourceLocation()
	if err != nil {
		return nil, err
	}

	s.initStatusToTransition()
	return s, nil
}

// Create is used to create all the credentialspec resources for a given task
func (cs *CredentialSpecResource) Create() error {
	var err error
	var iamCredentials credentials.IAMRoleCredentials

	executionCredentials, ok := cs.credentialsManager.GetTaskCredentials(cs.getExecutionCredentialsID())
	if ok {
		iamCredentials = executionCredentials.GetIAMRoleCredentials()
	}

	for credSpecStr := range cs.credentialSpecContainerMap {
		credSpecSplit := strings.SplitAfterN(credSpecStr, ":", 2)
		if len(credSpecSplit) != 2 {
			seelog.Errorf("Invalid credentialspec: %s", credSpecStr)
			continue
		}
		credSpecPrefix := credSpecSplit[0]
		if credSpecPrefix == "credentialspecdomainless:" {
			cs.isDomainlessGMSATask = true
		}
		credSpecValue := credSpecSplit[1]

		if strings.HasPrefix(credSpecValue, "file://") {
			err = cs.handleCredentialspecFile(credSpecStr)
			if err != nil {
				seelog.Errorf("Failed to handle the credentialspec file: %v", err)
				cs.setTerminalReason(err.Error())
				return err
			}
			continue
		}

		parsedARN, err := arn.Parse(credSpecValue)
		if err != nil {
			cs.setTerminalReason(err.Error())
			return err
		}

		parsedARNService := parsedARN.Service
		if parsedARNService == "s3" {
			err = cs.handleS3CredentialspecFile(credSpecStr, credSpecValue, iamCredentials)
			if err != nil {
				seelog.Errorf("Failed to handle the credentialspec file from s3: %v", err)
				cs.setTerminalReason(err.Error())
				return err
			}
		} else if parsedARNService == "ssm" {
			err = cs.handleSSMCredentialspecFile(credSpecStr, credSpecValue, iamCredentials)
			if err != nil {
				seelog.Errorf("Failed to handle the credentialspec file from SSM: %v", err)
				cs.setTerminalReason(err.Error())
				return err
			}
		} else {
			err = errors.New("unsupported credentialspec ARN, only s3/ssm ARNs are valid")
			cs.setTerminalReason(err.Error())
			return err
		}
	}

	if cs.isDomainlessGMSATask {
		// The domainless gMSA Windows Plugin needs the execution role credentials to pull customer secrets
		err = setTaskExecutionCredentialsRegKeysImpl(iamCredentials, cs.CredentialSpecResourceCommon.taskARN)
		if err != nil {
			cs.setTerminalReason(err.Error())
			return err
		}
	}

	return nil
}

func (cs *CredentialSpecResource) handleCredentialspecFile(credentialspec string) error {
	credSpecSplit := strings.SplitAfterN(credentialspec, ":", 2)
	if len(credSpecSplit) != 2 {
		seelog.Errorf("Invalid credentialspec: %s", credentialspec)
		return errors.New("invalid credentialspec file specification")
	}
	credSpecPrefix := credSpecSplit[0]
	credSpecFile := credSpecSplit[1]

	if !strings.HasPrefix(credSpecFile, "file://") {
		return errors.New("invalid credentialspec file specification")
	}

	if credSpecPrefix == "credentialspecdomainless:" {
		relativeFilePath := strings.TrimPrefix(credSpecFile, "file://")
		dir, originalFileName := filepath.Split(relativeFilePath)

		// Generate unique filename using taskId, containerName, credspecfile original name
		taskId, err := internalarn.TaskIdFromArn(cs.taskARN)
		if err != nil {
			cs.setTerminalReason(err.Error())
			return err
		}
		containerName, ok := cs.credentialSpecContainerMap[credentialspec]
		if !ok {
			return errors.New(fmt.Sprintf("Unable to retrieve containerName from credentialSpecContainerMap. No such key %s", credentialspec))
		}

		// We need a different outfile in order to avoid modifying the customers original credentialspec
		outFile := fmt.Sprintf("%s_%s_%s", taskId, containerName, originalFileName)
		credSpecFile = "file://" + filepath.Join(dir, outFile)

		// Fill in appropriate domainless gMSA fields
		err = readWriteDomainlessCredentialSpecImpl(filepath.Join(cs.credentialSpecResourceLocation, dir, originalFileName), filepath.Join(cs.credentialSpecResourceLocation, dir, outFile), cs.taskARN)
		if err != nil {
			cs.setTerminalReason(err.Error())
			return err
		}
	}

	dockerHostconfigSecOptCredSpec := "credentialspec=" + credSpecFile
	cs.updateCredSpecMapping(credentialspec, dockerHostconfigSecOptCredSpec)

	return nil
}

func (cs *CredentialSpecResource) handleS3CredentialspecFile(originalCredentialspec, credentialspecS3ARN string, iamCredentials credentials.IAMRoleCredentials) error {
	if iamCredentials == (credentials.IAMRoleCredentials{}) {
		err := errors.New("credentialspec resource: unable to find execution role credentials")
		cs.setTerminalReason(err.Error())
		return err
	}

	parsedARN, err := arn.Parse(credentialspecS3ARN)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	bucket, key, err := s3.ParseS3ARN(credentialspecS3ARN)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	s3Client, err := cs.s3ClientCreator.NewS3ManagerClient(bucket, cs.region, iamCredentials, cs.ipCompatibility)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	resourceBase := filepath.Base(parsedARN.Resource)
	taskArnSplit := strings.Split(cs.taskARN, "/")
	length := len(taskArnSplit)
	if length < 2 {
		return errors.New("Failed to retrieve taskId from taskArn.")
	}

	localCredSpecFilePath := fmt.Sprintf("%s\\s3_%v_%s", cs.credentialSpecResourceLocation, taskArnSplit[length-1], resourceBase)
	err = cs.writeS3File(func(file oswrapper.File) error {
		return s3.DownloadFile(bucket, key, s3DownloadTimeout, file, s3Client)
	}, localCredSpecFilePath)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	err = handleNonFileDomainlessGMSACredSpecImpl(originalCredentialspec, localCredSpecFilePath, cs.taskARN)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	dockerHostconfigSecOptCredSpec := fmt.Sprintf("credentialspec=file://%s", filepath.Base(localCredSpecFilePath))
	cs.updateCredSpecMapping(originalCredentialspec, dockerHostconfigSecOptCredSpec)

	return nil
}

func (cs *CredentialSpecResource) handleSSMCredentialspecFile(originalCredentialspec, credentialspecSSMARN string, iamCredentials credentials.IAMRoleCredentials) error {
	if iamCredentials == (credentials.IAMRoleCredentials{}) {
		err := errors.New("credentialspec resource: unable to find execution role credentials")
		cs.setTerminalReason(err.Error())
		return err
	}

	parsedARN, err := arn.Parse(credentialspecSSMARN)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	ssmClient, err := cs.ssmClientCreator.NewSSMClient(cs.region, iamCredentials, cs.ipCompatibility)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	// An SSM ARN is in the form of arn:aws:ssm:us-west-2:123456789012:parameter/a/b. The parsed ARN value
	// would be parameter/a/b. The following code gets the SSM parameter by passing "/a/b" value to the
	// GetParametersFromSSM method to retrieve the value in the parameter.
	ssmParam := strings.SplitAfterN(parsedARN.Resource, "parameter", 2)
	if len(ssmParam) != 2 {
		err := fmt.Errorf("the provided SSM parameter:%s is in an invalid format", parsedARN.Resource)
		cs.setTerminalReason(err.Error())
		return err
	}
	ssmParams := []string{ssmParam[1]}
	ssmParamMap, err := ssm.GetParametersFromSSM(ssmParams, ssmClient)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	ssmParamData := ssmParamMap[ssmParam[1]]
	taskArnSplit := strings.Split(cs.taskARN, "/")
	length := len(taskArnSplit)
	if length < 2 {
		return errors.New("Failed to retrieve taskId from taskArn.")
	}

	taskId := taskArnSplit[length-1]
	containerName := cs.credentialSpecContainerMap[originalCredentialspec]

	// We compose a string that is a concatenation of the task_id, container name and the ARN of the credential spec
	// SSM parameter. This concatenated string is hashed using the SHA-256 hashing scheme to generate a fixed length
	// checksum string of 64 characters. This helps with resolving collisions within a host or a task using the same SSM
	// parameter.
	credSpecFileNameHashFormat := fmt.Sprintf("%s%s%s", taskId, containerName, credentialspecSSMARN)
	hashFunction := sha256.New()
	hashFunction.Write([]byte(credSpecFileNameHashFormat))
	customCredSpecFileName := fmt.Sprintf("%x", hashFunction.Sum(nil))

	localCredSpecFilePath := filepath.Join(cs.credentialSpecResourceLocation, customCredSpecFileName)
	err = cs.writeSSMFile(ssmParamData, localCredSpecFilePath)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	err = handleNonFileDomainlessGMSACredSpecImpl(originalCredentialspec, localCredSpecFilePath, cs.taskARN)
	if err != nil {
		cs.setTerminalReason(err.Error())
		return err
	}

	dockerHostconfigSecOptCredSpec := fmt.Sprintf("credentialspec=file://%s", customCredSpecFileName)
	cs.updateCredSpecMapping(originalCredentialspec, dockerHostconfigSecOptCredSpec)

	return nil
}

var rename = os.Rename

func (cs *CredentialSpecResource) writeS3File(writeFunc func(file oswrapper.File) error, filePath string) error {
	temp, err := cs.ioutil.TempFile(cs.credentialSpecResourceLocation, tempFileName)
	if err != nil {
		return err
	}

	err = writeFunc(temp)
	if err != nil {
		return err
	}

	err = temp.Close()
	if err != nil {
		seelog.Errorf("Error while closing the handle to file %s: %v", temp.Name(), err)
		return err
	}

	err = rename(temp.Name(), filePath)
	if err != nil {
		seelog.Errorf("Error while renaming the temporary file %s: %v", temp.Name(), err)
		return err
	}
	return nil
}

func (cs *CredentialSpecResource) writeSSMFile(ssmParamData, filePath string) error {
	return cs.ioutil.WriteFile(filePath, []byte(ssmParamData), filePerm)
}

func (cs *CredentialSpecResource) updateCredSpecMapping(credSpecInput, targetCredSpec string) {
	cs.lock.Lock()
	defer cs.lock.Unlock()

	seelog.Debugf("Updating credentialspec mapping for %s with %s", credSpecInput, targetCredSpec)
	cs.CredSpecMap[credSpecInput] = targetCredSpec
}

// Cleanup removes the credentialspec created for the task
func (cs *CredentialSpecResource) Cleanup() error {
	cs.clearCredentialSpec()
	if cs.isDomainlessGMSATask {
		err := cs.deleteTaskExecutionCredentialsRegKeys()
		if err != nil {
			return err
		}
	}
	return nil
}

// clearCredentialSpec cycles through the collection of credentialspec data and
// removes them from the task
func (cs *CredentialSpecResource) clearCredentialSpec() {
	cs.lock.Lock()
	defer cs.lock.Unlock()

	for key, value := range cs.CredSpecMap {
		if strings.HasPrefix(key, "credentialspec:file://") {
			seelog.Debugf("Skipping cleanup of local credentialspec file option: %s", key)
			continue
		}
		// Split credentialspec to obtain local file-name
		credSpecSplit := strings.SplitAfterN(value, "file://", 2)
		if len(credSpecSplit) != 2 {
			seelog.Warnf("Unable to parse target credentialspec: %s", value)
			continue
		}
		localCredentialSpecFile := credSpecSplit[1]
		localCredentialSpecFilePath := filepath.Join(cs.credentialSpecResourceLocation, localCredentialSpecFile)
		err := osRemoveImpl(localCredentialSpecFilePath)
		if err != nil {
			seelog.Warnf("Unable to clear local credential spec file %s for task %s", localCredentialSpecFile, cs.taskARN)
		}

		delete(cs.CredSpecMap, key)
	}
}

func (cs *CredentialSpecResource) deleteTaskExecutionCredentialsRegKeys() error {
	cs.lock.Lock()
	defer cs.lock.Unlock()

	return deleteTaskExecutionCredentialsRegKeysImpl(cs.taskARN)
}

// deleteTaskExecutionCredentialsRegKeys deletes the taskExecutionRole IAM credentials in the task registry key
// after the task has been terminated.
func deleteTaskExecutionCredentialsRegKeys(taskARN string) error {
	k, err := registry.OpenKey(registry.LOCAL_MACHINE, ecsCcgPluginRegistryKeyRoot, registry.ALL_ACCESS)
	if err != nil {
		// Early exit with success case, if the registry key doesn't exist then there are no task execution role creds to cleanup
		seelog.Errorf("Error opening %s key: %s", ecsCcgPluginRegistryKeyRoot, err)
		return nil
	}
	defer k.Close()

	err = registry.DeleteKey(k, taskARN)
	if err != nil {
		seelog.Errorf("Error deleting %s key: %s", ecsCcgPluginRegistryKeyRoot+"\\"+taskARN, err)
		return err
	}
	seelog.Infof("Deleted Task Execution Credential Registry key for task: %s", taskARN)
	return nil
}

func (cs *CredentialSpecResource) setCredentialSpecResourceLocation() error {
	// TODO: Use registry to setup credentialspec resource location
	// This should always be available on Windows instances
	programDataDir := os.Getenv(envProgramData)
	if programDataDir != "" {
		// Sample resource location: C:\ProgramData\docker\credentialspecs
		cs.credentialSpecResourceLocation = filepath.Join(programDataDir, dockerCredentialSpecDataDir)
	}

	if cs.credentialSpecResourceLocation == "" {
		return errors.New("credentialspec resource location not available")
	}

	return nil
}

// CredentialSpecResourceJSON is the json representation of the credentialspec resource
type CredentialSpecResourceJSON struct {
	*CredentialSpecResourceJSONCommon
}

func (cs *CredentialSpecResource) MarshallPlatformSpecificFields(credentialSpecResourceJSON *CredentialSpecResourceJSON) {
	return
}

func (cs *CredentialSpecResource) UnmarshallPlatformSpecificFields(credentialSpecResourceJSON CredentialSpecResourceJSON) {
	return
}

// setTaskExecutionCredentialsRegKeys stores the taskExecutionRole IAM credentials to the task registry key
// so that the domainless gMSA plugin may use these credentials to access the customer Active Directory authentication
// information.
func SetTaskExecutionCredentialsRegKeys(taskCredentials credentials.IAMRoleCredentials, taskArn string) error {
	if taskCredentials == (credentials.IAMRoleCredentials{}) {
		err := errors.New("Unable to find execution role credentials while setting registry key for task " + taskArn)
		return err
	}

	taskRegistryKey, _, err := registry.CreateKey(registry.LOCAL_MACHINE, ecsCcgPluginRegistryKeyRoot+"\\"+taskArn, registry.WRITE)
	if err != nil {
		errMsg := fmt.Sprintf("Error creating registry key root %s for task %s: %s", ecsCcgPluginRegistryKeyRoot, taskArn, err)
		seelog.Error(errMsg)
		return errors.Wrap(err, errMsg)
	}
	defer taskRegistryKey.Close()

	err = taskRegistryKey.SetStringValue("AKID", taskCredentials.AccessKeyID)
	if err != nil {
		errMsg := fmt.Sprintf("Error creating AKID child value for task %s:%s", taskArn, err)
		seelog.Error(errMsg)
		return errors.Wrap(err, errMsg)
	}
	err = taskRegistryKey.SetStringValue("SKID", taskCredentials.SecretAccessKey)
	if err != nil {
		errMsg := fmt.Sprintf("Error creating AKID child value for task %s:%s", taskArn, err)
		seelog.Error(errMsg)
		return errors.Wrap(err, errMsg)
	}
	err = taskRegistryKey.SetStringValue("SESSIONTOKEN", taskCredentials.SessionToken)
	if err != nil {
		errMsg := fmt.Sprintf("Error creating SESSIONTOKEN child value for task %s:%s", taskArn, err)
		seelog.Errorf(errMsg)
		return errors.Wrap(err, errMsg)
	}

	seelog.Infof("Successfully SetTaskExecutionCredentialsRegKeys for task %s", taskArn)
	return nil
}

// handleNonFileDomainlessGMSACredSpec reads and then injects the taskExecutionRoleRegistryKey location for
// the s3/ssm gMSA credential spec cases.
func handleNonFileDomainlessGMSACredSpec(originalCredSpec, localCredSpecFilePath, taskARN string) error {
	// Exit early for non domainless gMSA cred specs
	if !strings.HasPrefix(originalCredSpec, "credentialspecdomainless:") {
		return nil
	}

	err := readWriteDomainlessCredentialSpecImpl(localCredSpecFilePath, localCredSpecFilePath, taskARN)
	if err != nil {
		return err
	}
	return nil
}

// readWriteDomainlessCredentialSpec is used to open the credential spec file on local disk, inject the
// taskExecutionRoleInformation in memory, and then write the file to a specific path. The reason we do not
// modify the same fail is to avoid modifying the customer resource when the customer provides a local file
// credential spec
func readWriteDomainlessCredentialSpec(filePath, outFilePath, taskARN string) error {
	credSpec, err := readCredentialSpecImpl(filePath)
	if err != nil {
		return err
	}
	err = writeCredentialSpecImpl(credSpec, outFilePath, taskARN)
	if err != nil {
		return err
	}
	return nil
}

// readCredentialSpec is used to open the credential spec file on local disk and read it into a generic
// bytes map object map[string]interface{}
func readCredentialSpec(filePath string) (map[string]interface{}, error) {
	byteResult, err := osReadFileImpl(filePath)
	if err != nil {
		return nil, err
	}
	var credSpec map[string]interface{}
	err = json.Unmarshal(byteResult, &credSpec)
	if err != nil {
		return nil, err
	}
	return credSpec, nil
}

// writeCredentialSpec is used to selectively decode portions of the Microsoft gMSA generated credential spec file and then
// inject the taskExecutionRoleRegistryKey location so that the gMSA plugin is able to access these IAM credentials.
// The reason that the JSON unmarshalling is manual is to protect against future key/value pairs that appear in the JSON,
// while only modifying the portions that pertain to domainless gMSA. This is in case Microsoft adds additional keys to the
// JSON credential spec, so that our writer does not ignore this data.
func writeCredentialSpec(credSpec map[string]interface{}, outFilePath string, taskARN string) error {
	activeDirectoryConfigUntyped, ok := credSpec["ActiveDirectoryConfig"]
	if !ok {
		return errors.New(fmt.Sprintf(credentialSpecParseErrorMsgTemplate, "ActiveDirectoryConfig"))
	}
	activeDirectoryConfig, ok := activeDirectoryConfigUntyped.(map[string]interface{})
	if !ok {
		return errors.New(fmt.Sprintf(untypedMarshallErrorMsgTemplate, "activeDirectoryConfigUntyped", "map[string]interface{}"))
	}

	hostAccountConfigUntyped, ok := activeDirectoryConfig["HostAccountConfig"]
	if !ok {
		return errors.New(fmt.Sprintf(credentialSpecParseErrorMsgTemplate, "HostAccountConfig"))
	}
	hostAccountConfig, ok := hostAccountConfigUntyped.(map[string]interface{})
	if !ok {
		return errors.New(fmt.Sprintf(untypedMarshallErrorMsgTemplate, "hostAccountConfigUntyped", "map[string]interface{}"))
	}

	pluginInputStringUntyped, ok := hostAccountConfig["PluginInput"]
	if !ok {
		return errors.New(fmt.Sprintf(credentialSpecParseErrorMsgTemplate, "PluginInput"))
	}
	var pluginInputParsed pluginInput
	pluginInputString, ok := pluginInputStringUntyped.(string)
	if !ok {
		return errors.New(fmt.Sprintf(untypedMarshallErrorMsgTemplate, "pluginInputStringUntyped", "string"))
	}
	err := json.Unmarshal([]byte(pluginInputString), &pluginInputParsed)
	if err != nil {
		return err
	}

	pluginInputParsed.RegKeyPath = fmt.Sprintf(regKeyPathFormat, taskARN)

	pluginInputBytes, err := json.Marshal(pluginInputParsed)
	if err != nil {
		return err
	}

	hostAccountConfig["PluginInput"] = string(pluginInputBytes)

	jsonBytes, err := json.Marshal(credSpec)
	if err != nil {
		return err
	}

	err = osWriteFileImpl(outFilePath, jsonBytes, filePerm)
	if err != nil {
		return err
	}

	return nil
}
