|
| 1 | +/* |
| 2 | + * Copyright (c) 2024. Devtron Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +package pipeline |
| 18 | + |
| 19 | +import ( |
| 20 | + "errors" |
| 21 | + "testing" |
| 22 | + |
| 23 | + commonBean "github.com/devtron-labs/common-lib/workflow" |
| 24 | + repository2 "github.com/devtron-labs/devtron/pkg/plugin/repository" |
| 25 | + "github.com/go-pg/pg" |
| 26 | + "github.com/stretchr/testify/assert" |
| 27 | + "go.uber.org/zap" |
| 28 | +) |
| 29 | + |
| 30 | +// fakePluginRepoForWfRequest serves a plugin definition (steps, script, variables, conditions) to the workflow request |
| 31 | +// builders. Any repository method not overridden here panics if called, which is intended. |
| 32 | +type fakePluginRepoForWfRequest struct { |
| 33 | + repository2.GlobalPluginRepository |
| 34 | + steps []*repository2.PluginStep |
| 35 | + scripts map[int]*repository2.PluginPipelineScript |
| 36 | + variables map[int][]*repository2.PluginStepVariable |
| 37 | + variablesErr error |
| 38 | + conditions map[int][]*repository2.PluginStepCondition |
| 39 | + conditionCalls int |
| 40 | +} |
| 41 | + |
| 42 | +func (f *fakePluginRepoForWfRequest) GetStepsByPluginIds(pluginIds []int) ([]*repository2.PluginStep, error) { |
| 43 | + return f.steps, nil |
| 44 | +} |
| 45 | + |
| 46 | +func (f *fakePluginRepoForWfRequest) GetScriptDetailById(id int) (*repository2.PluginPipelineScript, error) { |
| 47 | + if script, ok := f.scripts[id]; ok { |
| 48 | + return script, nil |
| 49 | + } |
| 50 | + return nil, pg.ErrNoRows |
| 51 | +} |
| 52 | + |
| 53 | +func (f *fakePluginRepoForWfRequest) GetScriptMappingDetailByScriptId(scriptId int) ([]*repository2.ScriptPathArgPortMapping, error) { |
| 54 | + return nil, nil |
| 55 | +} |
| 56 | + |
| 57 | +func (f *fakePluginRepoForWfRequest) GetVariablesByStepId(stepId int) ([]*repository2.PluginStepVariable, error) { |
| 58 | + if f.variablesErr != nil { |
| 59 | + return nil, f.variablesErr |
| 60 | + } |
| 61 | + return f.variables[stepId], nil |
| 62 | +} |
| 63 | + |
| 64 | +func (f *fakePluginRepoForWfRequest) GetConditionsByStepId(stepId int) ([]*repository2.PluginStepCondition, error) { |
| 65 | + f.conditionCalls++ |
| 66 | + return f.conditions[stepId], nil |
| 67 | +} |
| 68 | + |
| 69 | +func newPluginConditionsTestService(repo *fakePluginRepoForWfRequest) *PipelineStageServiceImpl { |
| 70 | + return &PipelineStageServiceImpl{logger: zap.NewNop().Sugar(), globalPluginRepository: repo} |
| 71 | +} |
| 72 | + |
| 73 | +func pluginStepVariable(name string, variableType repository2.PluginStepVariableType, exposed bool, value, defaultValue string) *repository2.PluginStepVariable { |
| 74 | + return &repository2.PluginStepVariable{ |
| 75 | + Id: 1, |
| 76 | + Name: name, |
| 77 | + Format: repository2.PLUGIN_VARIABLE_FORMAT_TYPE_STRING, |
| 78 | + VariableType: variableType, |
| 79 | + ValueType: repository2.PLUGIN_VARIABLE_VALUE_TYPE_NEW, |
| 80 | + IsExposed: exposed, |
| 81 | + Value: value, |
| 82 | + DefaultValue: defaultValue, |
| 83 | + VariableStepIndex: 1, |
| 84 | + } |
| 85 | +} |
| 86 | + |
| 87 | +// pluginWithConditions models a custom plugin saved from the dashboard: one shell step, an exposed input variable |
| 88 | +// FILE_PATH with a TRIGGER condition on it and an exposed output variable CONTENT with a PASS condition on it. |
| 89 | +func pluginWithConditions() *fakePluginRepoForWfRequest { |
| 90 | + input := pluginStepVariable("FILE_PATH", repository2.PLUGIN_VARIABLE_TYPE_INPUT, true, "/tmp/shivam", "") |
| 91 | + input.Id = 386 |
| 92 | + output := pluginStepVariable("CONTENT", repository2.PLUGIN_VARIABLE_TYPE_OUTPUT, true, "", "") |
| 93 | + output.Id = 387 |
| 94 | + return &fakePluginRepoForWfRequest{ |
| 95 | + steps: []*repository2.PluginStep{ |
| 96 | + {Id: 69, PluginId: 71, Name: "Task 1", Index: 1, StepType: repository2.PLUGIN_STEP_TYPE_INLINE, ScriptId: 5}, |
| 97 | + }, |
| 98 | + scripts: map[int]*repository2.PluginPipelineScript{5: {Type: repository2.SCRIPT_TYPE_SHELL, Script: "echo hi > $FILE_PATH"}}, |
| 99 | + variables: map[int][]*repository2.PluginStepVariable{69: {input, output}}, |
| 100 | + conditions: map[int][]*repository2.PluginStepCondition{69: { |
| 101 | + {Id: 23, PluginStepId: 69, ConditionVariableId: 386, ConditionType: repository2.PLUGIN_CONDITION_TYPE_TRIGGER, ConditionalOperator: "==", ConditionalValue: "/tmp/shivam"}, |
| 102 | + {Id: 24, PluginStepId: 69, ConditionVariableId: 387, ConditionType: repository2.PLUGIN_CONDITION_TYPE_SUCCESS, ConditionalOperator: "==", ConditionalValue: "ok"}, |
| 103 | + }}, |
| 104 | + } |
| 105 | +} |
| 106 | + |
| 107 | +func TestBuildPluginStepDataForWfRequest_PluginConditionsAreNotSentToCiRunner(t *testing.T) { |
| 108 | + t.Run("inline plugin step: variables and script are sent, plugin-defined conditions are not", func(t *testing.T) { |
| 109 | + repo := pluginWithConditions() |
| 110 | + stepData, err := newPluginConditionsTestService(repo).BuildPluginStepDataForWfRequest(repo.steps[0]) |
| 111 | + assert.NoError(t, err) |
| 112 | + assert.Equal(t, "Task 1", stepData.Name) |
| 113 | + assert.Equal(t, 1, stepData.Index) |
| 114 | + assert.Equal(t, string(repository2.PLUGIN_STEP_TYPE_INLINE), stepData.StepType) |
| 115 | + assert.Equal(t, string(repository2.SCRIPT_TYPE_SHELL), stepData.ExecutorType) |
| 116 | + assert.Equal(t, "echo hi > $FILE_PATH", stepData.Script) |
| 117 | + if assert.Len(t, stepData.InputVars, 1) { |
| 118 | + assert.Equal(t, "FILE_PATH", stepData.InputVars[0].Name) |
| 119 | + assert.Equal(t, "/tmp/shivam", stepData.InputVars[0].Value) |
| 120 | + } |
| 121 | + if assert.Len(t, stepData.OutputVars, 1) { |
| 122 | + assert.Equal(t, "CONTENT", stepData.OutputVars[0].Name) |
| 123 | + } |
| 124 | + assert.Nil(t, stepData.TriggerSkipConditions, "plugin-level trigger/skip conditions must not reach ci-runner") |
| 125 | + assert.Nil(t, stepData.SuccessFailureConditions, "plugin-level pass/fail conditions must not reach ci-runner") |
| 126 | + assert.Equal(t, 0, repo.conditionCalls, "plugin_step_condition must not even be read for the workflow request") |
| 127 | + }) |
| 128 | + t.Run("plugin step referencing another plugin: no conditions either", func(t *testing.T) { |
| 129 | + repo := pluginWithConditions() |
| 130 | + nested := &repository2.PluginStep{Id: 69, PluginId: 71, Name: "nested", Index: 1, StepType: repository2.PLUGIN_STEP_TYPE_REF_PLUGIN, RefPluginId: 42} |
| 131 | + stepData, err := newPluginConditionsTestService(repo).BuildPluginStepDataForWfRequest(nested) |
| 132 | + assert.NoError(t, err) |
| 133 | + assert.Equal(t, "PLUGIN", stepData.ExecutorType) |
| 134 | + assert.Equal(t, 42, stepData.RefPluginId) |
| 135 | + assert.Len(t, stepData.InputVars, 1) |
| 136 | + assert.Nil(t, stepData.TriggerSkipConditions) |
| 137 | + assert.Nil(t, stepData.SuccessFailureConditions) |
| 138 | + assert.Equal(t, 0, repo.conditionCalls) |
| 139 | + }) |
| 140 | + t.Run("variable lookup error is propagated", func(t *testing.T) { |
| 141 | + repo := pluginWithConditions() |
| 142 | + repo.variablesErr = errors.New("db down") |
| 143 | + stepData, err := newPluginConditionsTestService(repo).BuildPluginStepDataForWfRequest(repo.steps[0]) |
| 144 | + assert.EqualError(t, err, "db down") |
| 145 | + assert.Nil(t, stepData) |
| 146 | + }) |
| 147 | +} |
| 148 | + |
| 149 | +func TestGetRefPluginStepsByIds_NoPluginConditionOnAnyStep(t *testing.T) { |
| 150 | + repo := pluginWithConditions() |
| 151 | + pluginIdStepsMap, err := newPluginConditionsTestService(repo).GetRefPluginStepsByIds([]int{71}, map[int]bool{71: true}) |
| 152 | + assert.NoError(t, err) |
| 153 | + if assert.Len(t, pluginIdStepsMap[71], 1) { |
| 154 | + assert.Nil(t, pluginIdStepsMap[71][0].TriggerSkipConditions) |
| 155 | + assert.Nil(t, pluginIdStepsMap[71][0].SuccessFailureConditions) |
| 156 | + assert.Len(t, pluginIdStepsMap[71][0].InputVars, 1) |
| 157 | + } |
| 158 | + assert.Equal(t, 0, repo.conditionCalls) |
| 159 | +} |
| 160 | + |
| 161 | +func TestBuildPluginVariableDataForWfRequest(t *testing.T) { |
| 162 | + newRepo := func(vars ...*repository2.PluginStepVariable) *fakePluginRepoForWfRequest { |
| 163 | + return &fakePluginRepoForWfRequest{variables: map[int][]*repository2.PluginStepVariable{69: vars}} |
| 164 | + } |
| 165 | + t.Run("exposed input without default uses the stored value, non-exposed input uses its default", func(t *testing.T) { |
| 166 | + exposed := pluginStepVariable("A", repository2.PLUGIN_VARIABLE_TYPE_INPUT, true, "user", "") |
| 167 | + exposed.VariableStepIndexInPlugin = 3 // only meaningful for plugins nested in plugins; must be passed through untouched |
| 168 | + internal := pluginStepVariable("B", repository2.PLUGIN_VARIABLE_TYPE_INPUT, false, "ignored", "default") |
| 169 | + exposedWithDefault := pluginStepVariable("C", repository2.PLUGIN_VARIABLE_TYPE_INPUT, true, "", "default") |
| 170 | + inputs, outputs, err := newPluginConditionsTestService(newRepo(exposed, internal, exposedWithDefault)).BuildPluginVariableDataForWfRequest(69) |
| 171 | + assert.NoError(t, err) |
| 172 | + assert.Empty(t, outputs) |
| 173 | + if assert.Len(t, inputs, 3) { |
| 174 | + assert.Equal(t, "user", inputs[0].Value) |
| 175 | + assert.Equal(t, "default", inputs[1].Value) |
| 176 | + assert.Equal(t, "", inputs[2].Value, "exposed variable value is filled from the pipeline step by ci-runner") |
| 177 | + assert.Equal(t, commonBean.VariableTypeValue, inputs[0].VariableType) |
| 178 | + assert.Equal(t, commonBean.FormatTypeString, inputs[0].Format) |
| 179 | + assert.Equal(t, 3, inputs[0].VariableStepIndexInPlugin, "nested-plugin step index is passed through") |
| 180 | + assert.Equal(t, 0, inputs[1].VariableStepIndexInPlugin) |
| 181 | + } |
| 182 | + }) |
| 183 | + t.Run("value types map to ci-runner variable types", func(t *testing.T) { |
| 184 | + global := pluginStepVariable("G", repository2.PLUGIN_VARIABLE_TYPE_INPUT, false, "", "") |
| 185 | + global.ValueType = repository2.PLUGIN_VARIABLE_VALUE_TYPE_GLOBAL |
| 186 | + global.ReferenceVariableName = "DOCKER_IMAGE" |
| 187 | + previous := pluginStepVariable("P", repository2.PLUGIN_VARIABLE_TYPE_INPUT, false, "", "") |
| 188 | + previous.ValueType = repository2.PLUGIN_VARIABLE_VALUE_TYPE_PREVIOUS |
| 189 | + previous.PreviousStepIndex = 1 |
| 190 | + previous.ReferenceVariableName = "OUT" |
| 191 | + exposedPrevious := pluginStepVariable("E", repository2.PLUGIN_VARIABLE_TYPE_INPUT, true, "", "") |
| 192 | + exposedPrevious.ValueType = repository2.PLUGIN_VARIABLE_VALUE_TYPE_PREVIOUS |
| 193 | + inputs, _, err := newPluginConditionsTestService(newRepo(global, previous, exposedPrevious)).BuildPluginVariableDataForWfRequest(69) |
| 194 | + assert.NoError(t, err) |
| 195 | + if assert.Len(t, inputs, 3) { |
| 196 | + assert.Equal(t, commonBean.VariableTypeRefGlobal, inputs[0].VariableType) |
| 197 | + assert.Equal(t, "DOCKER_IMAGE", inputs[0].ReferenceVariableName) |
| 198 | + assert.Equal(t, commonBean.VariableTypeRefPlugin, inputs[1].VariableType) |
| 199 | + assert.Equal(t, 1, inputs[1].ReferenceVariableStepIndex) |
| 200 | + assert.Equal(t, commonBean.VariableTypeValue, inputs[2].VariableType, "exposed previous-step variables are resolved at pipeline level") |
| 201 | + } |
| 202 | + }) |
| 203 | + t.Run("output variables are returned separately", func(t *testing.T) { |
| 204 | + out := pluginStepVariable("OUT", repository2.PLUGIN_VARIABLE_TYPE_OUTPUT, true, "", "") |
| 205 | + inputs, outputs, err := newPluginConditionsTestService(newRepo(out)).BuildPluginVariableDataForWfRequest(69) |
| 206 | + assert.NoError(t, err) |
| 207 | + assert.Empty(t, inputs) |
| 208 | + if assert.Len(t, outputs, 1) { |
| 209 | + assert.Equal(t, "OUT", outputs[0].Name) |
| 210 | + } |
| 211 | + }) |
| 212 | + t.Run("no variables (pg.ErrNoRows) yields empty lists", func(t *testing.T) { |
| 213 | + repo := &fakePluginRepoForWfRequest{variablesErr: pg.ErrNoRows} |
| 214 | + inputs, outputs, err := newPluginConditionsTestService(repo).BuildPluginVariableDataForWfRequest(69) |
| 215 | + assert.NoError(t, err) |
| 216 | + assert.Empty(t, inputs) |
| 217 | + assert.Empty(t, outputs) |
| 218 | + }) |
| 219 | + t.Run("repository error is propagated", func(t *testing.T) { |
| 220 | + repo := &fakePluginRepoForWfRequest{variablesErr: errors.New("db down")} |
| 221 | + _, _, err := newPluginConditionsTestService(repo).BuildPluginVariableDataForWfRequest(69) |
| 222 | + assert.EqualError(t, err, "db down") |
| 223 | + }) |
| 224 | + t.Run("invalid variable format is an error", func(t *testing.T) { |
| 225 | + bad := pluginStepVariable("X", repository2.PLUGIN_VARIABLE_TYPE_INPUT, true, "", "") |
| 226 | + bad.Format = "BOGUS" |
| 227 | + _, _, err := newPluginConditionsTestService(newRepo(bad)).BuildPluginVariableDataForWfRequest(69) |
| 228 | + assert.Error(t, err) |
| 229 | + }) |
| 230 | +} |
0 commit comments