github.com/pf-qiu/concourse/v6@v6.7.3-0.20201207032516-1f455d73275f/atc/exec/set_pipeline_step.go (about) 1 package exec 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "strings" 10 11 "code.cloudfoundry.org/lager" 12 "code.cloudfoundry.org/lager/lagerctx" 13 "sigs.k8s.io/yaml" 14 15 "github.com/concourse/baggageclaim" 16 "github.com/pf-qiu/concourse/v6/atc" 17 "github.com/pf-qiu/concourse/v6/atc/configvalidate" 18 "github.com/pf-qiu/concourse/v6/atc/creds" 19 "github.com/pf-qiu/concourse/v6/atc/db" 20 "github.com/pf-qiu/concourse/v6/atc/exec/artifact" 21 "github.com/pf-qiu/concourse/v6/atc/exec/build" 22 "github.com/pf-qiu/concourse/v6/atc/policy" 23 "github.com/pf-qiu/concourse/v6/atc/worker" 24 "github.com/pf-qiu/concourse/v6/tracing" 25 "github.com/pf-qiu/concourse/v6/vars" 26 ) 27 28 const ActionRunSetPipeline = "SetPipeline" 29 30 // SetPipelineStep sets a pipeline to current team. This step takes pipeline 31 // configure file and var files from some resource in the pipeline, like git. 32 type SetPipelineStep struct { 33 planID atc.PlanID 34 plan atc.SetPipelinePlan 35 metadata StepMetadata 36 delegateFactory SetPipelineStepDelegateFactory 37 teamFactory db.TeamFactory 38 buildFactory db.BuildFactory 39 client worker.Client 40 policyChecker policy.Checker 41 } 42 43 func NewSetPipelineStep( 44 planID atc.PlanID, 45 plan atc.SetPipelinePlan, 46 metadata StepMetadata, 47 delegateFactory SetPipelineStepDelegateFactory, 48 teamFactory db.TeamFactory, 49 buildFactory db.BuildFactory, 50 client worker.Client, 51 policyChecker policy.Checker, 52 ) Step { 53 return &SetPipelineStep{ 54 planID: planID, 55 plan: plan, 56 metadata: metadata, 57 delegateFactory: delegateFactory, 58 teamFactory: teamFactory, 59 buildFactory: buildFactory, 60 client: client, 61 policyChecker: policyChecker, 62 } 63 } 64 65 func (step *SetPipelineStep) Run(ctx context.Context, state RunState) (bool, error) { 66 delegate := step.delegateFactory.SetPipelineStepDelegate(state) 67 ctx, span := delegate.StartSpan(ctx, "set_pipeline", tracing.Attrs{ 68 "name": step.plan.Name, 69 }) 70 71 ok, err := step.run(ctx, state, delegate) 72 tracing.End(span, err) 73 74 return ok, err 75 } 76 77 func (step *SetPipelineStep) run(ctx context.Context, state RunState, delegate SetPipelineStepDelegate) (bool, error) { 78 logger := lagerctx.FromContext(ctx) 79 logger = logger.Session("set-pipeline-step", lager.Data{ 80 "step-name": step.plan.Name, 81 "job-id": step.metadata.JobID, 82 }) 83 84 delegate.Initializing(logger) 85 86 interpolatedPlan, err := creds.NewSetPipelinePlan(state, step.plan).Evaluate() 87 if err != nil { 88 return false, err 89 } 90 step.plan = interpolatedPlan 91 92 stdout := delegate.Stdout() 93 stderr := delegate.Stderr() 94 95 fmt.Fprintln(stderr, "\x1b[1;33mWARNING: the set_pipeline step is experimental and subject to change!\x1b[0m") 96 fmt.Fprintln(stderr, "") 97 fmt.Fprintln(stderr, "\x1b[33mfollow RFC #31 for updates: https://github.com/concourse/rfcs/pull/31\x1b[0m") 98 fmt.Fprintln(stderr, "") 99 100 if step.plan.Name == "self" { 101 fmt.Fprintln(stderr, "\x1b[1;33mWARNING: 'set_pipeline: self' is experimental and may be removed in the future!\x1b[0m") 102 fmt.Fprintln(stderr, "") 103 fmt.Fprintln(stderr, "\x1b[33mcontribute to discussion #5732 with feedback: https://github.com/pf-qiu/concourse/v6/discussions/5732\x1b[0m") 104 fmt.Fprintln(stderr, "") 105 106 step.plan.Name = step.metadata.PipelineName 107 step.plan.InstanceVars = step.metadata.PipelineInstanceVars 108 // self must be set to current team, thus ignore team. 109 step.plan.Team = "" 110 } 111 112 source := setPipelineSource{ 113 ctx: ctx, 114 logger: logger, 115 step: step, 116 repo: state.ArtifactRepository(), 117 client: step.client, 118 } 119 120 err = source.Validate() 121 if err != nil { 122 return false, err 123 } 124 125 atcConfig, err := source.FetchPipelineConfig() 126 if err != nil { 127 return false, err 128 } 129 130 delegate.Starting(logger) 131 132 warnings, errors := configvalidate.Validate(atcConfig) 133 for _, warning := range warnings { 134 fmt.Fprintf(stderr, "WARNING: %s\n", warning.Message) 135 } 136 137 if len(errors) > 0 { 138 fmt.Fprintln(delegate.Stderr(), "invalid pipeline:") 139 140 for _, e := range errors { 141 fmt.Fprintf(stderr, "- %s", e) 142 } 143 144 delegate.Finished(logger, false) 145 return false, nil 146 } 147 148 var team db.Team 149 if step.plan.Team == "" { 150 team = step.teamFactory.GetByID(step.metadata.TeamID) 151 } else { 152 fmt.Fprintln(stderr, "\x1b[1;33mWARNING: specifying the team in a set_pipeline step is experimental and may be removed in the future!\x1b[0m") 153 fmt.Fprintln(stderr, "") 154 fmt.Fprintln(stderr, "\x1b[33mcontribute to discussion #5731 with feedback: https://github.com/pf-qiu/concourse/v6/discussions/5731\x1b[0m") 155 fmt.Fprintln(stderr, "") 156 157 currentTeam, found, err := step.teamFactory.FindTeam(step.metadata.TeamName) 158 if err != nil { 159 return false, err 160 } 161 if !found { 162 return false, fmt.Errorf("team %s not found", step.metadata.TeamName) 163 } 164 165 targetTeam, found, err := step.teamFactory.FindTeam(step.plan.Team) 166 if err != nil { 167 return false, err 168 } 169 if !found { 170 return false, fmt.Errorf("team %s not found", step.plan.Team) 171 } 172 173 permitted := false 174 if targetTeam.ID() == currentTeam.ID() { 175 permitted = true 176 } 177 if currentTeam.Admin() { 178 permitted = true 179 } 180 if !permitted { 181 return false, fmt.Errorf( 182 "only %s team can set another team's pipeline", 183 atc.DefaultTeamName, 184 ) 185 } 186 187 team = targetTeam 188 } 189 190 pipelineRef := atc.PipelineRef{ 191 Name: step.plan.Name, 192 InstanceVars: step.plan.InstanceVars, 193 } 194 pipeline, found, err := team.Pipeline(pipelineRef) 195 if err != nil { 196 return false, err 197 } 198 199 fromVersion := db.ConfigVersion(0) 200 var existingConfig atc.Config 201 if !found { 202 existingConfig = atc.Config{} 203 } else { 204 fromVersion = pipeline.ConfigVersion() 205 existingConfig, err = pipeline.Config() 206 if err != nil { 207 return false, err 208 } 209 } 210 211 diffExists := existingConfig.Diff(stdout, atcConfig) 212 if !diffExists { 213 logger.Debug("no-diff") 214 215 fmt.Fprintf(stdout, "no changes to apply.\n") 216 217 if found { 218 err := pipeline.SetParentIDs(step.metadata.JobID, step.metadata.BuildID) 219 if err != nil { 220 return false, err 221 } 222 } 223 224 delegate.SetPipelineChanged(logger, false) 225 delegate.Finished(logger, true) 226 return true, nil 227 } 228 229 // conditionally check step 230 if step.policyChecker != nil && step.policyChecker.ShouldCheckAction(ActionRunSetPipeline) { 231 input := policy.PolicyCheckInput{ 232 Action: ActionRunSetPipeline, 233 Team: team.Name(), 234 Pipeline: step.plan.Name, 235 Data: &atcConfig, 236 } 237 result, err := step.policyChecker.Check(input) 238 if err != nil { 239 return false, fmt.Errorf("error checking policy enforcement") 240 } 241 if !result.Allowed { 242 return false, fmt.Errorf("policy check failed for set_pipeline: %s", strings.Join(result.Reasons, ", ")) 243 } 244 logger.Debug("policy check passed for set_pipeline") 245 } 246 247 fmt.Fprintf(stdout, "setting pipeline: %s\n", pipelineRef.String()) 248 delegate.SetPipelineChanged(logger, true) 249 250 parentBuild, found, err := step.buildFactory.Build(step.metadata.BuildID) 251 if err != nil { 252 return false, err 253 } 254 255 if !found { 256 return false, fmt.Errorf("set_pipeline step not attached to a buildID") 257 } 258 259 pipeline, _, err = parentBuild.SavePipeline(pipelineRef, team.ID(), atcConfig, fromVersion, false) 260 if err != nil { 261 if err == db.ErrSetByNewerBuild { 262 fmt.Fprintln(stderr, "\x1b[1;33mWARNING: the pipeline was not saved because it was already saved by a newer build\x1b[0m") 263 delegate.Finished(logger, true) 264 return true, nil 265 } 266 return false, err 267 } 268 269 fmt.Fprintf(stdout, "done\n") 270 logger.Info("saved-pipeline", lager.Data{"team": team.Name(), "pipeline": pipeline.Name()}) 271 delegate.Finished(logger, true) 272 273 return true, nil 274 } 275 276 type setPipelineSource struct { 277 ctx context.Context 278 logger lager.Logger 279 repo *build.Repository 280 step *SetPipelineStep 281 client worker.Client 282 } 283 284 func (s setPipelineSource) Validate() error { 285 if s.step.plan.File == "" { 286 return errors.New("file is not specified") 287 } 288 289 if !atc.EnablePipelineInstances && s.step.plan.InstanceVars != nil { 290 return errors.New("support for `instance_vars` is disabled") 291 } 292 293 return nil 294 } 295 296 // FetchConfig streams pipeline config file and var files from other resources 297 // and construct an atc.Config object 298 func (s setPipelineSource) FetchPipelineConfig() (atc.Config, error) { 299 config, err := s.fetchPipelineBits(s.step.plan.File) 300 if err != nil { 301 return atc.Config{}, err 302 } 303 304 staticVars := []vars.Variables{} 305 if len(s.step.plan.Vars) > 0 { 306 staticVars = append(staticVars, vars.StaticVariables(s.step.plan.Vars)) 307 } 308 for _, lvf := range s.step.plan.VarFiles { 309 bytes, err := s.fetchPipelineBits(lvf) 310 if err != nil { 311 return atc.Config{}, err 312 } 313 314 sv := vars.StaticVariables{} 315 err = yaml.Unmarshal(bytes, &sv) 316 if err != nil { 317 return atc.Config{}, err 318 } 319 320 staticVars = append(staticVars, sv) 321 } 322 323 if len(s.step.plan.InstanceVars) > 0 { 324 iv := vars.StaticVariables{} 325 for k, v := range s.step.plan.InstanceVars { 326 iv[k] = v 327 } 328 staticVars = append(staticVars, iv) 329 } 330 331 if len(staticVars) > 0 { 332 config, err = vars.NewTemplateResolver(config, staticVars).Resolve(false, false) 333 if err != nil { 334 return atc.Config{}, err 335 } 336 } 337 338 atcConfig := atc.Config{} 339 err = atc.UnmarshalConfig(config, &atcConfig) 340 if err != nil { 341 return atc.Config{}, err 342 } 343 344 return atcConfig, nil 345 } 346 347 func (s setPipelineSource) fetchPipelineBits(path string) ([]byte, error) { 348 segs := strings.SplitN(path, "/", 2) 349 if len(segs) != 2 { 350 return nil, UnspecifiedArtifactSourceError{path} 351 } 352 353 artifactName := segs[0] 354 filePath := segs[1] 355 356 stream, err := s.retrieveFromArtifact(artifactName, filePath) 357 if err != nil { 358 return nil, err 359 } 360 defer stream.Close() 361 362 byteConfig, err := ioutil.ReadAll(stream) 363 if err != nil { 364 return nil, err 365 } 366 367 return byteConfig, nil 368 } 369 370 func (s setPipelineSource) retrieveFromArtifact(name, file string) (io.ReadCloser, error) { 371 art, found := s.repo.ArtifactFor(build.ArtifactName(name)) 372 if !found { 373 return nil, UnknownArtifactSourceError{build.ArtifactName(name), file} 374 } 375 376 stream, err := s.client.StreamFileFromArtifact(s.ctx, s.logger, art, file) 377 if err != nil { 378 if err == baggageclaim.ErrFileNotFound { 379 return nil, artifact.FileNotFoundError{ 380 Name: name, 381 FilePath: file, 382 } 383 } 384 385 return nil, err 386 } 387 388 return stream, nil 389 }