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  }