Skip to content
This repository has been archived by the owner on Jan 8, 2024. It is now read-only.

Feature: Update pipelines parser to evaluate input variables #4132

Merged
merged 5 commits into from
Oct 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/4132.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
pipelines: Add ability to evaluate input variables in pipelines stanzas.
```
28 changes: 20 additions & 8 deletions internal/cli/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -404,17 +404,29 @@ func (c *InitCommand) validateProject() bool {
}

sp := sg.Add("Registering all pipelines for project %q", ref.Project)
protoPipes, err := c.cfg.PipelineProtos()
if err != nil {
c.stepError(s, initStepPipeline, err)
return false
}
pipeNames := c.cfg.Pipelines()

// We do a shallow sync of pipelines on the init phase in the same way we
// shallow sync Applications. Prior to running a pipeline - we do a full
// HCL eval and protobuf sync that will upsert over this data.
for _, pn := range pipeNames {
sp.Update("Registering Pipeline %q with the server...", pn)

for _, pipeline := range protoPipes {
sp.Update("Registering Pipeline %q with the server...", pipeline.Name)
baseStep := map[string]*pb.Pipeline_Step{"root": {
Name: "root",
Kind: &pb.Pipeline_Step_Pipeline_{},
}}

_, err := client.UpsertPipeline(c.Ctx, &pb.UpsertPipelineRequest{
Pipeline: pipeline,
Pipeline: &pb.Pipeline{
Name: pn,
Owner: &pb.Pipeline_Project{
Project: &pb.Ref_Project{
Project: ref.Project,
},
},
Steps: baseStep,
},
})
if err != nil {
c.stepError(sp, initStepPipeline, err)
Expand Down
83 changes: 49 additions & 34 deletions internal/config/pipeline.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,12 @@ type hclPipeline struct {
Remain hcl.Body `hcl:",remain"`
}

// hclStep represents a raw HCL version of a step stanza in a pipeline config
type hclStep struct {
// Step are the step settings for pipelines
type Step struct {
Labels map[string]string `hcl:"labels,optional"`
Use *Use `hcl:"use,block"`

// Give this step a name
Name string `hcl:",label"`

// If set, this step will depend on the defined step. The default step
Expand All @@ -44,14 +48,24 @@ type hclStep struct {
// The OCI image to use for executing this step
ImageURL string `hcl:"image_url,optional"`

// The plugin to use for this Step
Use *Use `hcl:"use,block"`
// An optional embedded pipeline stanza
Pipeline *Pipeline `hcl:"pipeline,block"`

ctx *hcl.EvalContext

// Optional workspace scoping
Workspace string `hcl:"workspace,optional"`
}

// hclStep represents a raw HCL version of a step stanza in a pipeline config
type hclStep struct {
Name string `hcl:",label"`

// An optional embedded pipeline stanza
PipelineRaw *hclPipeline `hcl:"pipeline,block"`

// An optional embedded pipeline stanza
Workspace string `hcl:"workspace,optional"`
Body hcl.Body `hcl:",body"`
Remain hcl.Body `hcl:",remain"`
}

// Pipelines returns the id of all the defined pipelines
Expand Down Expand Up @@ -99,14 +113,12 @@ func (c *Config) Pipeline(id string, ctx *hcl.EvalContext) (*Pipeline, error) {
var steps []*Step
for _, stepRaw := range pipeline.StepRaw {
// turn stepRaw into a staged Step
s := Step{
ctx: ctx,
Name: stepRaw.Name,
DependsOn: stepRaw.DependsOn,
ImageURL: stepRaw.ImageURL,
Use: stepRaw.Use,
Workspace: stepRaw.Workspace,
var step Step
if diag := gohcl.DecodeBody(stepRaw.Body, finalizeContext(ctx), &step); diag.HasErrors() {
return nil, diag
}
step.ctx = ctx
step.Name = stepRaw.Name

// Parse a nested pipeline step if defined
// TODO(briancain): At the moment, we're supporting singly nestested Pipeline
Expand All @@ -118,13 +130,13 @@ func (c *Config) Pipeline(id string, ctx *hcl.EvalContext) (*Pipeline, error) {
return nil, diag
}

s.Pipeline = &Pipeline{
step.Pipeline = &Pipeline{
Name: stepRaw.PipelineRaw.Name,
ctx: ctx,
Config: c,
}
if s.Pipeline.Config != nil {
s.Pipeline.Config.ctx = ctx
if step.Pipeline.Config != nil {
step.Pipeline.Config.ctx = ctx
}

// Parse all the steps
Expand Down Expand Up @@ -154,21 +166,19 @@ func (c *Config) Pipeline(id string, ctx *hcl.EvalContext) (*Pipeline, error) {
}

// turn stepRaw into a staged Step
s := Step{
ctx: ctx,
Name: embedStepRaw.Name,
DependsOn: embedStepRaw.DependsOn,
ImageURL: embedStepRaw.ImageURL,
Use: embedStepRaw.Use,
Workspace: embedStepRaw.Workspace,
var embedStep Step
if diag := gohcl.DecodeBody(embedStepRaw.Body, finalizeContext(ctx), &embedStep); diag.HasErrors() {
return nil, diag
}
embSteps = append(embSteps, &s)
embedStep.ctx = ctx
embedStep.Name = embedStepRaw.Name
embSteps = append(embSteps, &embedStep)
}

s.Pipeline.Steps = embSteps
step.Pipeline.Steps = embSteps
}

steps = append(steps, &s)
steps = append(steps, &step)
}

pipeline.Steps = steps
Expand All @@ -191,7 +201,12 @@ func (c *Config) PipelineProtos() ([]*pb.Pipeline, error) {
// Load HCL config and convert to a Pipeline proto
var result []*pb.Pipeline
for _, pl := range c.hclConfig.Pipelines {
pipes, err := c.buildPipelineProto(pl)
pipeline, err := c.Pipeline(pl.Name, c.ctx)
if err != nil {
return nil, err
}

pipes, err := c.buildPipelineProto(pipeline)
if err != nil {
return nil, err
}
Expand All @@ -206,7 +221,7 @@ func (c *Config) PipelineProtos() ([]*pb.Pipeline, error) {

// buildPipelineProto will recursively translate an hclPipeline into a protobuf
// Pipeline message.
func (c *Config) buildPipelineProto(pl *hclPipeline) ([]*pb.Pipeline, error) {
func (c *Config) buildPipelineProto(pl *Pipeline) ([]*pb.Pipeline, error) {
var result []*pb.Pipeline
pipe := &pb.Pipeline{
Name: pl.Name,
Expand All @@ -218,7 +233,7 @@ func (c *Config) buildPipelineProto(pl *hclPipeline) ([]*pb.Pipeline, error) {
}

steps := make(map[string]*pb.Pipeline_Step)
for i, step := range pl.StepRaw {
for i, step := range pl.Steps {
s := &pb.Pipeline_Step{
Name: step.Name,
DependsOn: step.DependsOn,
Expand All @@ -233,31 +248,31 @@ func (c *Config) buildPipelineProto(pl *hclPipeline) ([]*pb.Pipeline, error) {

// If no dependency was explictily set, we rely on the previous step
if i != 0 && len(step.DependsOn) == 0 {
s.DependsOn = []string{pl.StepRaw[i-1].Name}
s.DependsOn = []string{pl.Steps[i-1].Name}
}

// We have an embeded pipeline for this step. This can either be an hclPipeline
// defined directly in the step, or a pipeline reference to another pipeline
// defined else where. If this is a ref, the raw hcl for the pipeline should
// be a "built-in" step of type "pipeline"
if step.PipelineRaw != nil {
if step.Pipeline != nil {
// Parse the embedded pipeline assuming it has steps
if len(step.PipelineRaw.StepRaw) > 0 {
if len(step.Pipeline.Steps) > 0 {
// This means this is an embedded pipeline, i.e. the HCL definition
// is nested within the step PipelineRaw. we parse that pipeline
// directly and store it as a separate pipeline, and make _this_ step
// a reference to the pipeline

// Parse nested pipeline steps
pipelines, err := c.buildPipelineProto(step.PipelineRaw)
pipelines, err := c.buildPipelineProto(step.Pipeline)
if err != nil {
return nil, err
}

result = append(result, pipelines...)

// We check if this step references a separate pipeline by Owner
pipeName := step.PipelineRaw.Name
pipeName := step.Pipeline.Name
pipeProject := c.hclConfig.Project

// Add pipeline reference as a pipeline ref step for parent pipeline
Expand Down
32 changes: 31 additions & 1 deletion internal/config/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"path/filepath"
"testing"

"github.com/hashicorp/go-hclog"
"github.com/hashicorp/waypoint/internal/config/variables"
pb "github.com/hashicorp/waypoint/pkg/server/gen"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -59,6 +61,29 @@ func TestPipeline(t *testing.T) {
},
},

{
"pipeline_input_var.hcl",
"foo",
func(t *testing.T, c *Pipeline) {
require := require.New(t)

require.NotNil(t, c)
require.Equal("foo", c.Name)

steps := c.Steps
s := steps[0]

var p testStepPluginConfig
diag := s.Configure(&p, nil)
if diag.HasErrors() {
t.Fatal(diag.Error())
}

require.NotEmpty(t, p.config.Foo)
require.Equal("example.com/test", s.ImageURL)
},
},

{
"pipeline_multi_step.hcl",
"foo",
Expand Down Expand Up @@ -222,7 +247,11 @@ func TestPipeline(t *testing.T) {
})
require.NoError(err)

pipeline, err := cfg.Pipeline(tt.Pipeline, nil)
evalCtx := EvalContext(nil, "").NewChild()
inputVars, _, _ := variables.EvaluateVariables(hclog.L(), nil, cfg.InputVariables, "")
AddVariables(evalCtx, inputVars)

pipeline, err := cfg.Pipeline(tt.Pipeline, evalCtx)
require.NoError(err)

tt.Func(t, pipeline)
Expand Down Expand Up @@ -306,6 +335,7 @@ func TestPipelineProtos(t *testing.T) {
require.Len(pipelines[0].Steps, 1)

nestedStep := pipelines[0].Steps["test_nested"]
require.NotNil(nestedStep)
require.Equal(nestedStep.Name, "test_nested")
},
},
Expand Down
25 changes: 0 additions & 25 deletions internal/config/stages.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,31 +41,6 @@ type scopedStage struct {
Remain hcl.Body `hcl:",remain"`
}

// Step are the step settings for pipelines
type Step struct {
Labels map[string]string `hcl:"labels,optional"`
Use *Use `hcl:"use,block"`

// Give this step a name
Name string `hcl:",label"`

// If set, this step will depend on the defined step. The default step
// will be the previously defined step in order that it was defined
// in a waypoint.hcl
DependsOn []string `hcl:"depends_on,optional"`

// The OCI image to use for executing this step
ImageURL string `hcl:"image_url,optional"`

// An optional embedded pipeline stanza
Pipeline *Pipeline `hcl:"pipeline,block"`

ctx *hcl.EvalContext

// Optional workspace scoping
Workspace string `hcl:"workspace,optional"`
}

// Build are the build settings.
type Build struct {
Labels map[string]string `hcl:"labels,optional"`
Expand Down
28 changes: 28 additions & 0 deletions internal/config/testdata/pipelines/pipeline_input_var.hcl
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
project = "foo"

pipeline "foo" {
step "test" {
image_url = var.image_url

use "test" {
foo = "bar"
}
}
}

app "web" {
config {
env = {
static = "hello"
}
}

build {}

deploy {}
}

variable "image_url" {
default = "example.com/test"
type = string
}
12 changes: 6 additions & 6 deletions internal/config/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,20 +326,20 @@ func (c *Config) validatePipeline(b *hcl.Block) []ValidationResult {
func (c *Pipeline) Validate() error {
var result error

for _, stepRaw := range c.StepRaw {
if stepRaw == nil {
for _, step := range c.Steps {
if step == nil {
result = multierror.Append(result, fmt.Errorf(
"step stage in pipeline is nil, this is an internal error"))
} else if stepRaw != nil && (stepRaw.Use == nil && stepRaw.PipelineRaw == nil) {
} else if step != nil && (step.Use == nil && step.Pipeline == nil) {
result = multierror.Append(result, fmt.Errorf(
"step stage with a default 'use' stanza or a 'pipeline' stanza is required"))
} else if stepRaw.Use != nil && stepRaw.PipelineRaw != nil {
} else if step.Use != nil && step.Pipeline != nil {
result = multierror.Append(result, fmt.Errorf(
"step stage with both a 'use' stanza and pipeline stanza is not valid"))
} else if stepRaw.PipelineRaw == nil && (stepRaw.Use == nil || stepRaw.Use.Type == "") {
} else if step.Pipeline == nil && (step.Use == nil || step.Use.Type == "") {
result = multierror.Append(result, fmt.Errorf(
"step stage %q is required to define a 'use' stanza and label or a "+
"pipeline stanza but neither were found", stepRaw.Name))
"pipeline stanza but neither were found", step.Name))
}

// else, other step validations?
Expand Down
3 changes: 2 additions & 1 deletion internal/core/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,9 @@ func NewProject(ctx context.Context, os ...Option) (*Project, error) {

// configure pipelines for project and its apps
for _, name := range opts.Config.Pipelines() {
// Set input variables for pipelines and steps in context
evalCtx := config.EvalContext(nil, p.dir.DataDir()).NewChild()
// TODO: Add variables
config.AddVariables(evalCtx, p.variables)

pipelineConfig, err := opts.Config.Pipeline(name, evalCtx)
if err != nil {
Expand Down