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 }