github.com/diggerhq/digger/libs@v0.0.0-20240604170430-9d61cdf01cc5/digger_config/converters.go (about)

     1  package digger_config
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  
     7  	"github.com/dominikbraun/graph"
     8  )
     9  
    10  const defaultWorkflowName = "default"
    11  
    12  // hard - even if dependency project wasn't changed, it will be executed
    13  // soft - if dependency project wasn't changed, it will be skipped
    14  const (
    15  	DependencyConfigurationHard = "hard"
    16  	DependencyConfigurationSoft = "soft"
    17  )
    18  
    19  func copyProjects(projects []*ProjectYaml) []Project {
    20  	result := make([]Project, len(projects))
    21  	for i, p := range projects {
    22  		driftDetection := true
    23  
    24  		if p.DriftDetection != nil {
    25  			driftDetection = *p.DriftDetection
    26  		}
    27  
    28  		var roleToAssume *AssumeRoleForProject = nil
    29  		if p.AwsRoleToAssume != nil {
    30  
    31  			// set a default region to us-east-1 the same default AWS uses
    32  			if p.AwsRoleToAssume.AwsRoleRegion == "" {
    33  				p.AwsRoleToAssume.AwsRoleRegion = "us-east-1"
    34  			}
    35  
    36  			if p.AwsRoleToAssume.State == "" {
    37  				p.AwsRoleToAssume.State = p.AwsRoleToAssume.Command
    38  			}
    39  			if p.AwsRoleToAssume.Command == "" {
    40  				p.AwsRoleToAssume.Command = p.AwsRoleToAssume.State
    41  			}
    42  
    43  			roleToAssume = &AssumeRoleForProject{
    44  				AwsRoleRegion: p.AwsRoleToAssume.AwsRoleRegion,
    45  				State:         p.AwsRoleToAssume.State,
    46  				Command:       p.AwsRoleToAssume.Command,
    47  			}
    48  		}
    49  
    50  		workflowFile := "digger_workflow.yml"
    51  		if p.WorkflowFile != nil {
    52  			workflowFile = *p.WorkflowFile
    53  		}
    54  
    55  		item := Project{p.Name,
    56  			p.Dir,
    57  			p.Workspace,
    58  			p.Terragrunt,
    59  			p.OpenTofu,
    60  			p.Workflow,
    61  			workflowFile,
    62  			p.IncludePatterns,
    63  			p.ExcludePatterns,
    64  			p.DependencyProjects,
    65  			driftDetection,
    66  			roleToAssume,
    67  		}
    68  		result[i] = item
    69  	}
    70  	return result
    71  }
    72  
    73  func copyTerraformEnvConfig(terraformEnvConfig *TerraformEnvConfigYaml) *TerraformEnvConfig {
    74  	if terraformEnvConfig == nil {
    75  		return &TerraformEnvConfig{}
    76  	}
    77  	result := TerraformEnvConfig{}
    78  	result.State = make([]EnvVar, len(terraformEnvConfig.State))
    79  	result.Commands = make([]EnvVar, len(terraformEnvConfig.Commands))
    80  
    81  	for i, s := range terraformEnvConfig.State {
    82  		item := EnvVar{
    83  			s.Name,
    84  			s.ValueFrom,
    85  			s.Value,
    86  		}
    87  		result.State[i] = item
    88  	}
    89  	for i, s := range terraformEnvConfig.Commands {
    90  		item := EnvVar{
    91  			s.Name,
    92  			s.ValueFrom,
    93  			s.Value,
    94  		}
    95  		result.Commands[i] = item
    96  	}
    97  
    98  	return &result
    99  }
   100  
   101  func copyStage(stage *StageYaml) *Stage {
   102  	result := Stage{}
   103  	result.Steps = make([]Step, len(stage.Steps))
   104  
   105  	for i, s := range stage.Steps {
   106  		item := Step{
   107  			Action:    s.Action,
   108  			Value:     s.Value,
   109  			ExtraArgs: s.ExtraArgs,
   110  			Shell:     s.Shell,
   111  		}
   112  		result.Steps[i] = item
   113  	}
   114  	return &result
   115  }
   116  
   117  func copyWorkflowConfiguration(config *WorkflowConfigurationYaml) *WorkflowConfiguration {
   118  	result := WorkflowConfiguration{}
   119  	result.OnPullRequestClosed = config.OnPullRequestClosed
   120  	result.OnPullRequestPushed = config.OnPullRequestPushed
   121  	result.OnCommitToDefault = config.OnCommitToDefault
   122  	result.OnPullRequestConvertedToDraft = config.OnPullRequestConvertedToDraft
   123  	return &result
   124  }
   125  
   126  // converts dict of WorkflowYaml's to dict of Workflow's
   127  func copyWorkflows(workflows map[string]*WorkflowYaml) map[string]Workflow {
   128  	result := make(map[string]Workflow, len(workflows))
   129  	for i, w := range workflows {
   130  		if w == nil {
   131  			item := *defaultWorkflow()
   132  			result[i] = item
   133  		} else {
   134  			envVars := copyTerraformEnvConfig(w.EnvVars)
   135  			plan := copyStage(w.Plan)
   136  			apply := copyStage(w.Apply)
   137  			configuration := copyWorkflowConfiguration(w.Configuration)
   138  			item := Workflow{
   139  				envVars,
   140  				plan,
   141  				apply,
   142  				configuration,
   143  			}
   144  			result[i] = item
   145  		}
   146  	}
   147  	return result
   148  }
   149  
   150  func ConvertDiggerYamlToConfig(diggerYaml *DiggerConfigYaml) (*DiggerConfig, graph.Graph[string, Project], error) {
   151  	var diggerConfig DiggerConfig
   152  
   153  	if diggerYaml.DependencyConfiguration != nil {
   154  		diggerConfig.DependencyConfiguration = DependencyConfiguration{
   155  			Mode: diggerYaml.DependencyConfiguration.Mode,
   156  		}
   157  	} else {
   158  		diggerConfig.DependencyConfiguration = DependencyConfiguration{
   159  			Mode: DependencyConfigurationHard,
   160  		}
   161  	}
   162  
   163  	if diggerYaml.AutoMerge != nil {
   164  		diggerConfig.AutoMerge = *diggerYaml.AutoMerge
   165  	} else {
   166  		diggerConfig.AutoMerge = false
   167  	}
   168  
   169  	if diggerYaml.ApplyAfterMerge != nil {
   170  		diggerConfig.ApplyAfterMerge = *diggerYaml.ApplyAfterMerge
   171  	} else {
   172  		diggerConfig.ApplyAfterMerge = false
   173  	}
   174  
   175  	if diggerYaml.CommentRenderMode != nil {
   176  		diggerConfig.CommentRenderMode = *diggerYaml.CommentRenderMode
   177  	} else {
   178  		diggerConfig.CommentRenderMode = CommentRenderModeBasic
   179  	}
   180  
   181  	if diggerYaml.MentionDriftedProjectsInPR != nil {
   182  		diggerConfig.MentionDriftedProjectsInPR = *diggerYaml.MentionDriftedProjectsInPR
   183  	} else {
   184  		diggerConfig.MentionDriftedProjectsInPR = false
   185  	}
   186  
   187  	if diggerYaml.Telemetry != nil {
   188  		diggerConfig.Telemetry = *diggerYaml.Telemetry
   189  	} else {
   190  		diggerConfig.Telemetry = true
   191  	}
   192  
   193  	if diggerYaml.TraverseToNestedProjects != nil {
   194  		diggerConfig.TraverseToNestedProjects = *diggerYaml.TraverseToNestedProjects
   195  	} else {
   196  		diggerConfig.TraverseToNestedProjects = false
   197  	}
   198  
   199  	if diggerYaml.AllowDraftPRs != nil {
   200  		diggerConfig.AllowDraftPRs = *diggerYaml.AllowDraftPRs
   201  	} else {
   202  		diggerConfig.AllowDraftPRs = false
   203  	}
   204  
   205  	// if workflow block is not specified in yaml we create a default one, and add it to every project
   206  	if diggerYaml.Workflows != nil {
   207  		workflows := copyWorkflows(diggerYaml.Workflows)
   208  		diggerConfig.Workflows = workflows
   209  
   210  		// provide default workflow if not specified
   211  		if _, ok := diggerConfig.Workflows[defaultWorkflowName]; !ok {
   212  			workflow := *defaultWorkflow()
   213  			diggerConfig.Workflows[defaultWorkflowName] = workflow
   214  		}
   215  	} else {
   216  		workflow := *defaultWorkflow()
   217  		diggerConfig.Workflows = make(map[string]Workflow)
   218  		diggerConfig.Workflows[defaultWorkflowName] = workflow
   219  	}
   220  
   221  	projects := copyProjects(diggerYaml.Projects)
   222  	diggerConfig.Projects = projects
   223  
   224  	// update project's workflow if needed
   225  	for _, project := range diggerConfig.Projects {
   226  		if project.Workflow == "" {
   227  			project.Workflow = defaultWorkflowName
   228  		}
   229  	}
   230  
   231  	// check for project name duplicates
   232  	projectNames := make(map[string]bool)
   233  	for _, project := range diggerConfig.Projects {
   234  		if projectNames[project.Name] {
   235  			return nil, nil, fmt.Errorf("project name '%s' is duplicated", project.Name)
   236  		}
   237  		projectNames[project.Name] = true
   238  	}
   239  
   240  	// check project dependencies exist
   241  	for _, project := range diggerConfig.Projects {
   242  		for _, dependency := range project.DependencyProjects {
   243  			if !projectNames[dependency] {
   244  				return nil, nil, fmt.Errorf("project '%s' depends on '%s' which does not exist", project.Name, dependency)
   245  			}
   246  		}
   247  	}
   248  
   249  	dependencyGraph, err := CreateProjectDependencyGraph(diggerConfig.Projects)
   250  	if err != nil {
   251  		return nil, nil, fmt.Errorf("failed to create project dependency graph: %s", err.Error())
   252  	}
   253  
   254  	// if one of the workflows is missing Plan or Apply we copy default values
   255  	for _, w := range diggerConfig.Workflows {
   256  		defaultWorkflow := *defaultWorkflow()
   257  		if w.Plan == nil {
   258  			w.Plan = defaultWorkflow.Plan
   259  		}
   260  		if w.Apply == nil {
   261  			w.Apply = defaultWorkflow.Apply
   262  		}
   263  	}
   264  
   265  	return &diggerConfig, dependencyGraph, nil
   266  }
   267  
   268  func CreateProjectDependencyGraph(projects []Project) (graph.Graph[string, Project], error) {
   269  	projectHash := func(p Project) string {
   270  		return p.Name
   271  	}
   272  
   273  	projectsMap := make(map[string]Project)
   274  	for _, project := range projects {
   275  		projectsMap[project.Name] = project
   276  	}
   277  
   278  	g := graph.New(projectHash, graph.Directed(), graph.PreventCycles())
   279  	for _, project := range projects {
   280  		_, err := g.Vertex(project.Name)
   281  
   282  		if errors.Is(err, graph.ErrVertexNotFound) {
   283  			err := g.AddVertex(project)
   284  			if err != nil {
   285  				return nil, err
   286  			}
   287  		}
   288  		for _, dependency := range project.DependencyProjects {
   289  			_, err := g.Vertex(dependency)
   290  
   291  			if errors.Is(err, graph.ErrVertexNotFound) {
   292  				dependencyProject, ok := projectsMap[dependency]
   293  				if !ok {
   294  					return nil, fmt.Errorf("project '%s' does not exist", dependency)
   295  				}
   296  				err := g.AddVertex(dependencyProject)
   297  
   298  				if err != nil {
   299  					return nil, err
   300  				}
   301  			}
   302  
   303  			err = g.AddEdge(dependency, project.Name)
   304  			if err != nil {
   305  				return nil, err
   306  			}
   307  		}
   308  	}
   309  	return g, nil
   310  }