github.com/keikoproj/manny@v0.0.0-20210726112440-8571e4c99ced/configurator/configurator.go (about)

     1  package configurator
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net/http"
     9  	"net/url"
    10  	"os"
    11  	"path"
    12  	"path/filepath"
    13  	"regexp"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/aws/aws-sdk-go/aws/session"
    19  	"github.com/aws/aws-sdk-go/service/s3/s3iface"
    20  	"go.uber.org/zap"
    21  	"gopkg.in/yaml.v3"
    22  
    23  	"github.com/aws/aws-sdk-go/aws"
    24  	"github.com/aws/aws-sdk-go/service/s3"
    25  	"github.com/aws/aws-sdk-go/service/s3/s3manager"
    26  	"github.com/imdario/mergo"
    27  	gitopsv1alpha1 "github.com/keikoproj/cloudresource-manager/api/v1alpha1"
    28  
    29  	"github.com/keikoproj/manny/config"
    30  	"github.com/keikoproj/manny/configurator/mocks"
    31  )
    32  
    33  const (
    34  	// MannyConfigName is a special name in manny that denotes the name of the configuration file
    35  	MannyConfigName = "config.yaml"
    36  
    37  	// EmptyResourceRegExp Regex details : Should start with "Resources".
    38  	//Can have space in between.
    39  	//Should have ":" delimiter.
    40  	//Can have space in between.
    41  	//Can have "{}" or not. Space allowed between curly braces.
    42  	EmptyResourceRegExp = "^(Resources(\"{0,1})(\\s*):(\\s*)({(\\s*)}){0,1})$"
    43  )
    44  
    45  var counter int
    46  var baseDir []string
    47  var AcceptedExtensions = []string{".yaml", ".yml", ".json"}
    48  
    49  type SL struct {
    50  	// stack lists
    51  	StackList []map[string]string
    52  }
    53  
    54  var s SL
    55  
    56  type CloudResources []*CloudResourceDeployment
    57  
    58  // Validate runs various validations against the deployments
    59  func (c CloudResources) Validate() error {
    60  	lookupTable := map[string]bool{}
    61  
    62  	for _, resource := range c {
    63  		// find duplicate stack names
    64  		name := resource.Spec.Cloudformation.Stackname
    65  		if lookupTable[name] {
    66  			return errors.New("duplicate stack name found: " + name)
    67  		}
    68  
    69  		lookupTable[name] = true
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  
    76  
    77  // Render returns a json or yaml byte array of the resources
    78  func (c CloudResources) Render(format string) (bytes []byte, err error) {
    79  	switch format {
    80  	case "json":
    81  		for _, d := range c {
    82  			deployment, err := json.Marshal(d)
    83  			if err != nil {
    84  				return nil, err
    85  			}
    86  
    87  			bytes = append(bytes, deployment...)
    88  		}
    89  	default:
    90  		for _, d := range c {
    91  			bytes = append(bytes, []byte("---\n")...)
    92  
    93  			deployment, err := yaml.Marshal(d)
    94  			if err != nil {
    95  				return nil, err
    96  			}
    97  
    98  			bytes = append(bytes, deployment...)
    99  		}
   100  	}
   101  
   102  	return
   103  }
   104  
   105  // CloudResourceDeployment is a Custom Resource duplicated from the GitOps controller.
   106  // We had to duplicate it here because the built-in Kubernetes types are made for machine processing
   107  // with JSON. In order to produce a YAML manifest that someone can read we have to represent that
   108  // structure in our code.
   109  type CloudResourceDeployment struct {
   110  	Kind       string                                     `yaml:"kind" json:"kind"`
   111  	APIVersion string                                     `yaml:"apiVersion" json:"apiVersion"`
   112  	Metadata   Metadata                                   `yaml:"metadata" json:"metadata"`
   113  	Spec       gitopsv1alpha1.CloudResourceDeploymentSpec `yaml:"spec" json:"spec"`
   114  }
   115  
   116  type Metadata struct {
   117  	Name        string            `yaml:"name" json:"name"`
   118  	Namespace   string            `yaml:"namespace,omitempty" json:"namespace,omitempty"`
   119  	Labels      map[string]string `yaml:"labels,omitempty" json:"labels,omitempty"`
   120  	Annotations map[string]string `yaml:"annotations,omitempty" json:"annotations,omitempty"`
   121  }
   122  
   123  type TemplatePath string
   124  
   125  func (t TemplatePath) Parse() Template {
   126  	var template Template
   127  
   128  	switch true {
   129  	// s3 handler
   130  	case strings.HasPrefix(t.String(), "s3://"):
   131  		template.Type = "s3"
   132  		template.Path = t.String()
   133  	// http(s) handler
   134  	case strings.HasPrefix(t.String(), "http://"):
   135  		template.Type = "http"
   136  		template.Path = t.String()
   137  	// default is the file handler
   138  	default:
   139  		template.Type = "file"
   140  		template.Path = strings.TrimPrefix(t.String(), "file://")
   141  	}
   142  
   143  	return template
   144  }
   145  
   146  func contains(arr []string, str string) bool {
   147  	for _, a := range arr {
   148  		if a == str {
   149  			return true
   150  		}
   151  	}
   152  	return false
   153  }
   154  
   155  func (m *MannyConfig) CheckForEmptyResource() (bool,error) {
   156  	regex,err := regexp.Compile(EmptyResourceRegExp)
   157  	if err != nil {
   158  		return true,err
   159  	}
   160  	// Don't generate manifests for empty resources
   161  	indexOfResource := strings.Index(string(m.CloudFormation), "Resource")
   162  	if indexOfResource == -1 {
   163  		return false,nil
   164  	}
   165  	resourceSubString := m.CloudFormation[indexOfResource:]
   166  
   167  	return regex.Match(resourceSubString),nil
   168  }
   169  
   170  func (m *MannyConfig) runResolvers(stackList []map[string]string, fPath string) error {
   171  	m.OutputParameters = map[string]string{}
   172  
   173  	for key, value := range m.InputParameters {
   174  		var actualValue string
   175  		m.OutputParameters[key] = value.Value
   176  		switch value.Tag {
   177  		case "!environment_variable":
   178  			actualValue = os.Getenv(value.Value)
   179  			m.OutputParameters[key] = actualValue
   180  		case "!file_contents":
   181  			target := filepath.Join(filepath.Dir(fPath), value.Value)
   182  			data, err := ioutil.ReadFile(target)
   183  			if err != nil {
   184  				return err
   185  			}
   186  			m.OutputParameters[key] = string(data)
   187  		case "!stack_output":
   188  			sOutput := strings.Split(value.Value, "::")
   189  			sOutput[0] = filepath.Base(sOutput[0])
   190  			for _, v := range stackList {
   191  				if v[sOutput[0]] != "" {
   192  					m.OutputParameters[key] = value.Tag + " " + v[sOutput[0]] + "::" + sOutput[1]
   193  					break
   194  				} else {
   195  					m.OutputParameters[key] = value.Tag + " " + value.Value
   196  				}
   197  			}
   198  		}
   199  	}
   200  
   201  	return nil
   202  }
   203  
   204  func (c Configurator) mannyConfigValidate(path string) bool {
   205  	inputConfigData, err := ioutil.ReadFile(path)
   206  	if err != nil {
   207  		return false
   208  	}
   209  
   210  	_, err = config.ConfigValidate(inputConfigData, config.Schemaconfig)
   211  	if err != nil {
   212  		c.logger.Info("mannyConfig validation failed", zap.Error(err))
   213  		return false
   214  	}
   215  
   216  	c.logger.Debug("mannyConfig validation successful", zap.String("mannyConfig", path))
   217  	return true
   218  }
   219  
   220  func (t TemplatePath) IsEmpty() bool {
   221  	return t == ""
   222  }
   223  
   224  func (t TemplatePath) String() string {
   225  	return string(t)
   226  }
   227  
   228  type Template struct {
   229  	Type    string `yaml:"type"`
   230  	Path    string `yaml:"path"`
   231  	Version string `yaml:"version"`
   232  	Name    string `yaml:"name"`
   233  }
   234  
   235  // MannyConfig is a manny config. This is only read in yaml but can the Custom Resource that it gets written to can
   236  // output in YAML or JSON.
   237  type MannyConfig struct {
   238  	// FoundAt is the directory where Manny found the config
   239  	FoundAt string
   240  	// Template is the CloudFormation template provider config.
   241  	// Can be one of "file", "s3"
   242  	Template Template `yaml:"template"`
   243  	// TemplatePath provides the same functionality as Template but in one parseable string
   244  	TemplatePath TemplatePath `yaml:"template_path"`
   245  	// SyncWave
   246  	SyncWave int `yaml:"syncwave,omitempty"`
   247  	// InputParameters map to CloudFormation parameter values
   248  	InputParameters map[string]yaml.Node `yaml:"parameters,omitempty"`
   249  	// OutputParameters are derived from input parameters when using resolvers. These values are the ones that show up
   250  	// in the custom resource.
   251  	OutputParameters map[string]string
   252  	// Tags are CloudFormation tags to apply
   253  	Tags map[string]string `yaml:"tags"`
   254  	// StackName is the stack name to be used during execution
   255  	StackName string `yaml:"stackname"`
   256  	// RoleArn is the role to execute the CloudFormation with
   257  	RoleArn string `yaml:"rolearn"`
   258  	// ServiceRole that Cloudformation will assume. This is to have more security.
   259  	ServiceRoleARN string `yaml:"servicerolearn"`
   260  	// Expiry duration of the STS credentials. Defaults to 15 minutes if not set.
   261  	Duration time.Duration `yaml:"duration"`
   262  	// Optional ExternalID to pass along, defaults to nil if not set.
   263  	ExternalID string `yaml:"externalID,omitempty"`
   264  	// Optional AccountID.
   265  	AccountID string `yaml:"acctnum"`
   266  	// Optional Environment.
   267  	Environment string `yaml:"env,omitempty"`
   268  	// If ExpiryWindow is 0 or less it will be ignored.
   269  	ExpiryWindow time.Duration `yaml:"expirywindow,omitempty"`
   270  	// Timeout support
   271  	Timeout int `yaml:"timeout"`
   272  	//Region support
   273  	Region string `yaml:"region"`
   274  	// Base refers to additional configuration that should be loaded
   275  	Base           string `yaml:"base"`
   276  	CloudFormation []byte
   277  }
   278  
   279  // Configurator builds final configuration from many configurations
   280  type Configurator struct {
   281  	// Bases is the list of configs to be merged
   282  	Bases []MannyConfig
   283  	// Global is the config with all bases consumed
   284  	Global MannyConfig
   285  	// Stacks are the stack specific configs
   286  	Stacks []MannyConfig
   287  	// Origin is the original path that contains a config.yaml
   288  	Origin string
   289  	// GitURL is the remote URL used by Git
   290  	GitURL string
   291  	// References is a list of bases used
   292  	References []string
   293  	// StackPrefix is the generated prefix of the stack
   294  	StackPrefix string
   295  	// StackTable is a reference table for stacks
   296  	StackTable map[string]bool
   297  
   298  	logger   *zap.Logger
   299  	s3Client s3iface.S3API
   300  }
   301  
   302  type Config struct {
   303  	Path     string
   304  	GitURL   string
   305  	Logger   *zap.Logger
   306  	S3Client s3iface.S3API
   307  }
   308  
   309  // New creates a new configurator
   310  func New(config Config) Configurator {
   311  	c := Configurator{
   312  		logger: config.Logger,
   313  		GitURL: config.GitURL,
   314  	}
   315  
   316  	// Set the origin to an absolute path
   317  	path, _ := filepath.Abs(config.Path)
   318  	c.Origin = path + "/"
   319  
   320  	switch config.S3Client.(type) {
   321  	case *mocks.S3API:
   322  		c.s3Client = config.S3Client
   323  	default:
   324  		// use user credentials unless otherwise specified
   325  		config.Logger.Debug("S3Client not set, setting default")
   326  
   327  		sess := session.Must(session.NewSessionWithOptions(session.Options{
   328  			Config: aws.Config{Region: aws.String("us-west-2")},
   329  			SharedConfigState: session.SharedConfigEnable,
   330  		}))
   331  
   332  		c.s3Client = s3.New(sess)
   333  	}
   334  
   335  	return c
   336  }
   337  
   338  func (c *Configurator) CreateDeployments() (CloudResources, error) {
   339  	// build the config
   340  	err := c.loadBases()
   341  	if err != nil {
   342  		return nil, err
   343  	}
   344  
   345  	// render a manifest for deployment
   346  	return c.loadStacks()
   347  }
   348  
   349  // loadBases resolves base configs into the Global config
   350  func (c *Configurator) loadBases() error {
   351  	// Abs() does not include a trailing slash
   352  	config := c.Origin + MannyConfigName
   353  
   354  	c.logger.Debug("Finding config", zap.String("path", config))
   355  
   356  	// check to see if the config file exists
   357  	_, err := os.Stat(config)
   358  	if !os.IsNotExist(err) {
   359  		// Unmarshal the given config to determine if there are bases and recurses them
   360  		if err := c.unmarshal(config); err != nil {
   361  			return err
   362  		}
   363  	}
   364  
   365  	// Merge all the configs into one, starting with the bases
   366  	c.Global, err = c.mergeBases(c.Bases)
   367  	if err != nil {
   368  		return err
   369  	}
   370  
   371  	c.determineStackPrefix()
   372  
   373  	c.logger.Debug("Merged base configs", zap.Int("BaseConfigs", len(c.Bases)),
   374  		zap.Any("Global Config", c.Global))
   375  
   376  	return nil
   377  }
   378  
   379  func (c *Configurator) determineStackPrefix() {
   380  	if counter == 0 {
   381  		baseDir = strings.Split(filepath.Clean(c.Origin), "/")
   382  	}
   383  	counter++
   384  	dir := strings.Split(filepath.Clean(c.Origin), "/")
   385  	a := len(dir) - len(baseDir)
   386  	if a >= 0{
   387  		c.StackPrefix = strings.Join(dir[len(dir)-(a):], "-")
   388  	}
   389  	c.logger.Debug("Stack prefix determined", zap.String("StackPrefix", c.StackPrefix))
   390  }
   391  
   392  func (c Configurator) mergeBases(bases []MannyConfig) (MannyConfig, error) {
   393  	var global MannyConfig
   394  
   395  	for _, config := range bases {
   396  		if err := mergo.Merge(&global, config); err != nil {
   397  			return MannyConfig{}, err
   398  		}
   399  	}
   400  
   401  	return global, nil
   402  }
   403  
   404  func (c *Configurator) unmarshal(parentPath string) error {
   405  	c.logger.Debug("Reading file", zap.String("path", parentPath))
   406  
   407  	data, err := ioutil.ReadFile(parentPath)
   408  	if err != nil {
   409  		return err
   410  	}
   411  
   412  	var config MannyConfig
   413  	if err := yaml.Unmarshal(data, &config); err != nil {
   414  		return err
   415  	}
   416  
   417  	config.FoundAt = filepath.Clean(filepath.Dir(parentPath))
   418  	c.logger.Debug("Storing base location", zap.String("path", config.FoundAt))
   419  
   420  	// Recurse bases
   421  	if config.Base != "" {
   422  		c.logger.Debug("Base detected", zap.String("path", config.Base))
   423  
   424  		// determine absolute path
   425  		target := filepath.Join(filepath.Dir(parentPath), config.Base)
   426  
   427  		c.logger.Debug("Determining target", zap.String("ConfigPath", config.Base),
   428  			zap.String("TargetPath", target))
   429  
   430  		// @ToDo: Convert to map/lookup table
   431  		for _, basePath := range c.References {
   432  			if target == basePath {
   433  				return errors.New("circular dependency found in " + target)
   434  			}
   435  		}
   436  
   437  		if len(c.References)+1 > 10 {
   438  			return errors.New("more than 10 referenced bases")
   439  		}
   440  
   441  		// track references
   442  		c.References = append(c.References, target)
   443  
   444  		if err := c.unmarshal(target); err != nil {
   445  			return err
   446  		}
   447  	}
   448  
   449  	c.Bases = append(c.Bases, config)
   450  
   451  	return nil
   452  }
   453  
   454  // loadStacks loads stacks from the local directory and creates CloudResourceDeployments from them
   455  func (c *Configurator) loadStacks() (CloudResources, error) {
   456  	var resources CloudResources
   457  
   458  	c.logger.Debug("Looking for stack configs", zap.String("path", c.Origin))
   459  
   460  	// find stack files in the origin directory
   461  	files, err := ioutil.ReadDir(c.Origin)
   462  	if err != nil {
   463  		return nil, err
   464  	}
   465  	// process VPC stacks first and then dir
   466  	var files_inorder, d []os.FileInfo
   467  	for i, f := range files {
   468  		if f.IsDir() {
   469  			d = append(d, files[i])
   470  		}else {
   471  			files_inorder = append(files_inorder, f)
   472  		}
   473  	}
   474  	files_inorder = append(files_inorder, d...)
   475  
   476  	// load the stack configs
   477  	for _, f := range files_inorder {
   478  		// Resolve relative directories
   479  		target := filepath.Join(filepath.Dir(c.Origin+"/"), f.Name())
   480  
   481  		if f.Name() == MannyConfigName {
   482  			continue
   483  		}
   484  
   485  		// Recurse sub directories
   486  		if f.IsDir() {
   487  			c.logger.Debug("Walking directory for config", zap.String("path", target))
   488  
   489  			config := New(Config{
   490  				Path:   target + "/",
   491  				Logger: c.logger,
   492  				GitURL: c.GitURL,
   493  			})
   494  
   495  			if err := config.loadBases(); err != nil {
   496  				c.logger.Info("Unable to load base from higher level directory", zap.Error(err))
   497  				continue
   498  			}
   499  
   500  			deployments, err := config.loadStacks()
   501  			if err != nil {
   502  				c.logger.Info("Unable to load stacks from higher level directory", zap.Error(err))
   503  				continue
   504  			}
   505  
   506  			resources = append(resources, deployments...)
   507  
   508  			continue
   509  		}
   510  
   511  		extension := filepath.Ext(target)
   512  		if contains(AcceptedExtensions, extension) && c.mannyConfigValidate(target) {
   513  			// Handle stack generation
   514  			c.logger.Debug("Reading stack config", zap.String("path", target))
   515  
   516  			data, err := ioutil.ReadFile(target)
   517  			if err != nil {
   518  				return nil, err
   519  			}
   520  
   521  			var config MannyConfig
   522  			// unmarshal the stack config
   523  			err = yaml.Unmarshal(data, &config)
   524  			if err != nil {
   525  				return nil, err
   526  			}
   527  
   528  			// If no stack name is found, generate one
   529  			m := make(map[string]string)
   530  			if config.StackName == "" {
   531  				config.StackName = strings.TrimSuffix(f.Name(), filepath.Ext(f.Name()))
   532  				if c.StackPrefix != "" {
   533  					config.StackName = c.StackPrefix + "-" + config.StackName
   534  				}
   535  				m[f.Name()] = config.StackName
   536  				s.StackList = append(s.StackList, m)
   537  			} else {
   538  				m[f.Name()] = config.StackName
   539  				s.StackList = append(s.StackList, m)
   540  			}
   541  
   542  			// determine whether the template block or template_path is used
   543  			if !config.TemplatePath.IsEmpty() {
   544  				config.Template = config.TemplatePath.Parse()
   545  			}
   546  
   547  			switch config.Template.Type {
   548  			case "file":
   549  				// Resolve relative directories
   550  				target := filepath.Join(filepath.Dir(target), config.Template.Path)
   551  
   552  				// Unmarshal the CloudFormation
   553  				config.CloudFormation, err = ioutil.ReadFile(target)
   554  				if err != nil {
   555  					return nil, err
   556  				}
   557  			case "http":
   558  				resp, err := http.Get(config.Template.Path)
   559  				if err != nil {
   560  					return nil, err
   561  				}
   562  				defer resp.Body.Close()
   563  				if resp.StatusCode == http.StatusOK {
   564  					config.CloudFormation, err = ioutil.ReadAll(resp.Body)
   565  					if err != nil {
   566  						return nil, err
   567  					}
   568  				} else {
   569  					c.logger.Debug("http handler template download: http_status_code", zap.String("http_status", resp.Status))
   570  				}
   571  			case "s3":
   572  				//Parsing s3 bucket and key
   573  				u, _ := url.Parse(config.Template.Path)
   574  				if u.Host == "" || u.Path == "/" || u.Path == "" {
   575  					return nil, errors.New("s3 Bucket or Key not found: " + config.Template.Path)
   576  				}
   577  
   578  				// Support s3 http url format
   579  				if strings.Contains(u.Host, ".s3.amazonaws.com") {
   580  					u.Host = strings.ReplaceAll(u.Host, ".s3.amazonaws.com", "")
   581  				}
   582  
   583  				// tmpfile
   584  				fPath, err := ioutil.TempFile("", "s3-"+path.Base(config.Template.Path))
   585  				if err != nil {
   586  					return nil, err
   587  				}
   588  				defer os.Remove(fPath.Name()) // clean up
   589  
   590  				//Download cfn template from s3
   591  				downloader := s3manager.NewDownloaderWithClient(c.s3Client)
   592  				_, err = downloader.Download(fPath,
   593  					&s3.GetObjectInput{
   594  						Bucket: aws.String(u.Host),
   595  						Key:    aws.String(u.Path),
   596  					})
   597  				if err != nil {
   598  					return nil, err
   599  				}
   600  
   601  				// Unmarshal the CloudFormation
   602  				config.CloudFormation, err = ioutil.ReadFile(fPath.Name())
   603  				if err != nil {
   604  					return nil, err
   605  				}
   606  			}
   607  			// Don't generate manifests for empty resources
   608  			resourceEmpty,err := config.CheckForEmptyResource()
   609  			if err != nil {
   610  				c.logger.Error("Error while check for resource count")
   611  			}
   612  			if !resourceEmpty {
   613  				c.Stacks = append(c.Stacks, config)
   614  			}
   615  
   616  		} else {
   617  			c.logger.Debug("Skipping file", zap.String("path", target))
   618  		}
   619  	}
   620  
   621  	r, err := c.generateCR(c.Stacks)
   622  	if err != nil {
   623  		return nil, err
   624  	}
   625  
   626  	resources = append(resources, r...)
   627  
   628  	return resources, nil
   629  }
   630  
   631  func (c Configurator) generateCR(stacks []MannyConfig) (CloudResources, error) {
   632  	var manifests CloudResources
   633  
   634  	c.logger.With(zap.String("GitRemote", c.GitURL)).Debug("Writing git remote")
   635  
   636  	for _, stack := range stacks {
   637  		// Run resolvers
   638  		if err := stack.runResolvers(s.StackList, c.Origin); err != nil {
   639  			return nil, err
   640  		}
   641  
   642  		// Merge the Global config and the Stack config
   643  		if err := mergo.Merge(&stack, c.Global); err != nil {
   644  			return nil, err
   645  		}
   646  		if stack.SyncWave != 0 {
   647  			manifests = append(manifests, &CloudResourceDeployment{
   648  				Kind:       "CloudResourceDeployment",
   649  				APIVersion: "cloudresource.keikoproj.io/v1alpha1",
   650  				Metadata: Metadata{
   651  					Name: stack.StackName,
   652  					Annotations: map[string]string{
   653  						"source": c.GitURL,
   654  						"argocd.argoproj.io/sync-wave": strconv.Itoa(stack.SyncWave),
   655  					},
   656  				},
   657  				Spec: gitopsv1alpha1.CloudResourceDeploymentSpec{
   658  					Cloudformation: &gitopsv1alpha1.StackSpec{
   659  						Parameters: stack.OutputParameters,
   660  						Tags:       stack.Tags,
   661  						Template:   fmt.Sprintf("%s", stack.CloudFormation),
   662  						Stackname:  stack.StackName,
   663  						CARole: gitopsv1alpha1.AssumeRoleProvider{
   664  							RoleARN:         stack.RoleArn,
   665  							RoleSessionName: "gitops-deployment",
   666  							ServiceRoleARN:	stack.ServiceRoleARN,
   667  							ExternalID:	stack.ExternalID,
   668  							AccountID:	stack.AccountID,
   669  							Environment: stack.Environment,
   670  							Duration:	stack.Duration,
   671  							ExpiryWindow: stack.ExpiryWindow,
   672  						},
   673  						Timeout: stack.Timeout,
   674  						Region: stack.Region,
   675  					},
   676  				},
   677  			})
   678  		} else {
   679  			manifests = append(manifests, &CloudResourceDeployment{
   680  				Kind:       "CloudResourceDeployment",
   681  				APIVersion: "cloudresource.keikoproj.io/v1alpha1",
   682  				Metadata: Metadata{
   683  					Name: stack.StackName,
   684  					Annotations: map[string]string{
   685  						"source": c.GitURL,
   686  					},
   687  				},
   688  				Spec: gitopsv1alpha1.CloudResourceDeploymentSpec{
   689  					Cloudformation: &gitopsv1alpha1.StackSpec{
   690  						Parameters: stack.OutputParameters,
   691  						Tags:       stack.Tags,
   692  						Template:   fmt.Sprintf("%s", stack.CloudFormation),
   693  						Stackname:  stack.StackName,
   694  						CARole: gitopsv1alpha1.AssumeRoleProvider{
   695  							RoleARN:         stack.RoleArn,
   696  							RoleSessionName: "gitops-deployment",
   697  							ServiceRoleARN:	stack.ServiceRoleARN,
   698  							ExternalID:	stack.ExternalID,
   699  							AccountID:	stack.AccountID,
   700  							Environment: stack.Environment,
   701  							Duration:	stack.Duration,
   702  							ExpiryWindow: stack.ExpiryWindow,
   703  						},
   704  						Timeout: stack.Timeout,
   705  						Region: stack.Region,
   706  					},
   707  				},
   708  			})
   709  		}
   710  	}
   711  
   712  	return manifests, nil
   713  }