Skip to content

Commit 7f9fe10

Browse files
committed
Add target package, Add conf validator for pipelines and targets
Signed-off-by: kunalvirwal <kunalvirwal@gmail.com>
1 parent 883e092 commit 7f9fe10

7 files changed

Lines changed: 222 additions & 9 deletions

File tree

cmd/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func initServices() {
1717

1818
logger := utils.NewLogger(utils.DebugLevel, true)
1919
pipelineService := pipeline.NewPipelineService(logger)
20-
20+
pipelineService.LoadPipeline("./examples/pipeline.yaml")
2121
// Initialize main application
2222
app := app.NewApp(pipelineService, logger)
2323

examples/pipeline.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ spec:
3636
target: cluster1
3737
files:
3838
- "services/api/compose.yml"
39-
- "..."
39+
- "...yaml"

examples/target.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,8 @@ metadata:
55
type: cluster # or server
66
spec:
77
host: prod1.example.com
8+
user: api
9+
port: 6443
810
# This secret should point to either the ssh key in case of server or the kube config in case of cluster
9-
access-key-secret: prod1-ssh-key
11+
access-key-secret: prod1-kube-conf
12+

internal/pipeline/loader.go

Lines changed: 123 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package pipeline
22

33
import (
44
"os"
5+
"strings"
56

7+
pipelineSteps "github.com/kunalvirwal/shogun-cd/internal/pipeline/steps"
68
"go.yaml.in/yaml/v3"
79
)
810

@@ -20,11 +22,130 @@ func (p *Service) LoadPipeline(path string) {
2022
}
2123

2224
// [TODO]: Validate the pipeline structure here if needed
25+
if !p.validatePipeline(&pipeline) {
26+
return
27+
}
2328

2429
for i, sw := range pipeline.Spec.Steps {
25-
p.logger.LogInfo("Step %d: Type=%s, Details=%+v", i+1, sw.Step.Type(), sw.Step)
30+
p.logger.Log("Step %d: Type=%s, Details=%+v", i+1, sw.Step.Type(), sw.Step)
2631
}
27-
p.logger.Log("Pipeline %v loaded successfully", pipeline.Metadata.Name)
32+
p.logger.LogInfo("Pipeline %v loaded successfully", pipeline.Metadata.Name)
2833

2934
// [TODO]: Further processing of the loaded pipeline, store into DB and all
3035
}
36+
37+
func (p *Service) validatePipeline(pipeline *Pipeline) bool {
38+
// [TODO]: Implement validation logic
39+
if pipeline.ApiVersion != "shogun.dev/v1" {
40+
p.logger.LogNewError("invalid apiVersion: %s", pipeline.ApiVersion)
41+
return false
42+
}
43+
if pipeline.Kind != PipelineKind {
44+
p.logger.LogNewError("invalid kind: %s", pipeline.Kind)
45+
return false
46+
}
47+
// [TODO]: Check is name is unique
48+
if pipeline.Metadata.Name == "" {
49+
p.logger.LogNewError("metadata.name cannot be empty")
50+
return false
51+
}
52+
if len(pipeline.Spec.Triggers) == 0 {
53+
p.logger.LogNewError("at least one trigger must be specified")
54+
return false
55+
}
56+
for _, trigger := range pipeline.Spec.Triggers {
57+
if trigger.Type == string(WebhookTriggerKind) {
58+
continue
59+
}
60+
if trigger.Type == string(GitChangesTriggerKind) {
61+
if len(trigger.Paths) == 0 {
62+
p.logger.LogNewError("git_changes trigger must specify atleast one path")
63+
return false
64+
}
65+
continue
66+
}
67+
p.logger.LogNewError("invalid trigger type")
68+
return false
69+
}
70+
71+
for i, sw := range pipeline.Spec.Steps {
72+
if sw.Step == nil {
73+
p.logger.LogNewError("step %d is nil", i+1)
74+
return false
75+
}
76+
77+
switch sw.Step.Type() {
78+
case pipelineSteps.MutateType:
79+
step := sw.Step.(*pipelineSteps.MutateStep)
80+
if !p.validTrigger(step.TriggerWhen, i) {
81+
return false
82+
}
83+
if step.File == "" {
84+
p.logger.LogNewError("step %d mutate file cannot be empty", i+1)
85+
return false
86+
}
87+
if step.UpdateField == "" {
88+
p.logger.LogNewError("step %d mutate update_field cannot be empty", i+1)
89+
return false
90+
}
91+
92+
case pipelineSteps.SyncType:
93+
step := sw.Step.(*pipelineSteps.SyncStep)
94+
if !p.validTrigger(step.TriggerWhen, i) {
95+
return false
96+
}
97+
if step.Target == "" {
98+
p.logger.LogNewError("step %d exec target cannot be empty", i+1)
99+
return false
100+
}
101+
102+
case pipelineSteps.ExecType:
103+
step := sw.Step.(*pipelineSteps.ExecStep)
104+
if !p.validTrigger(step.TriggerWhen, i) {
105+
return false
106+
}
107+
if step.Target == "" {
108+
p.logger.LogNewError("step %d exec target cannot be empty", i+1)
109+
return false
110+
}
111+
if len(step.Commands) == 0 {
112+
p.logger.LogNewError("step %d exec commands cannot be empty", i+1)
113+
return false
114+
}
115+
116+
case pipelineSteps.ApplyType:
117+
step := sw.Step.(*pipelineSteps.ApplyStep)
118+
if !p.validTrigger(step.TriggerWhen, i) {
119+
return false
120+
}
121+
if step.Target == "" {
122+
p.logger.LogNewError("step %d exec target cannot be empty", i+1)
123+
return false
124+
}
125+
if len(step.Files) == 0 {
126+
p.logger.LogNewError("step %d apply files cannot be empty", i+1)
127+
return false
128+
}
129+
for j, file := range step.Files {
130+
if !strings.HasSuffix(file, ".yaml") && !strings.HasSuffix(file, ".yml") && !strings.HasSuffix(file, ".json") {
131+
p.logger.LogNewError("step %d apply has a file with invalid extension at index %d", i+1, j)
132+
return false
133+
}
134+
}
135+
136+
default:
137+
p.logger.LogNewError("step %d has unknown type: %s", i+1, sw.Step.Type())
138+
return false
139+
}
140+
141+
}
142+
return true
143+
}
144+
145+
func (p *Service) validTrigger(trigger string, i int) bool {
146+
if trigger == "" || trigger == string(WebhookTriggerKind) || trigger == string(GitChangesTriggerKind) {
147+
return true
148+
}
149+
p.logger.LogNewError("step %d has invalid trigger_when: %s", i+1, trigger)
150+
return false
151+
}

internal/pipeline/pipeline.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import pipelineSteps "github.com/kunalvirwal/shogun-cd/internal/pipeline/steps"
44

55
type Kind string
66

7-
const PipelineKind Kind = "Pipeline"
7+
const (
8+
PipelineKind Kind = "Pipeline"
9+
WebhookTriggerKind Kind = "ci_webhook"
10+
GitChangesTriggerKind Kind = "git_changes"
11+
)
812

913
type Pipeline struct {
1014
ApiVersion string `yaml:"apiVersion"`

internal/pipeline/steps/apply.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package pipelineSteps
22

33
type ApplyStep struct {
4-
TriggerWhen string `yaml:"trigger_when,omitempty"`
5-
Target string `yaml:"target"` // [TODO]: change this to pointer if needed
6-
Files []string
4+
TriggerWhen string `yaml:"trigger_when,omitempty"`
5+
Target string `yaml:"target"` // [TODO]: change this to pointer if needed
6+
Files []string `yaml:"files"`
77
}
88

99
func (*ApplyStep) Type() string {

internal/target/target.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package target
2+
3+
import (
4+
"os"
5+
6+
"github.com/kunalvirwal/shogun-cd/internal/utils"
7+
"go.yaml.in/yaml/v3"
8+
)
9+
10+
type Kind string
11+
12+
const TargetKind Kind = "Target"
13+
14+
const (
15+
ServerType = "server"
16+
ClusterType = "cluster"
17+
)
18+
19+
type Target struct {
20+
ApiVersion string `yaml:"apiVersion"`
21+
Kind Kind `yaml:"kind"`
22+
Metadata Metadata `yaml:"metadata"`
23+
Spec Spec `yaml:"spec"`
24+
}
25+
26+
type Metadata struct {
27+
Name string `yaml:"name"`
28+
Type string `yaml:"type"` // server or cluster
29+
}
30+
31+
type Spec struct {
32+
Host string `yaml:"host"`
33+
User string `yaml:"user"`
34+
Port int `yaml:"port"`
35+
AccessSecret string `yaml:"access-key-secret"`
36+
}
37+
38+
func LoadTarget(logger utils.Logger, path string) *Target {
39+
data, err := os.ReadFile(path)
40+
if err != nil {
41+
logger.LogNewError("failed to read target file: %v", err)
42+
return nil
43+
}
44+
45+
var target Target
46+
if err := yaml.Unmarshal(data, &target); err != nil {
47+
logger.LogNewError("failed to unmarshal target YAML: %v", err)
48+
return nil
49+
}
50+
51+
// [TODO]: Validate the target structure here if needed
52+
if !validateTarget(logger, &target) {
53+
return nil
54+
}
55+
56+
logger.LogInfo("Target loaded: Name=%s, Type=%s, Host=%s", target.Metadata.Name, target.Metadata.Type, target.Spec.Host)
57+
58+
// [TODO]: Further processing of the loaded target, store into DB and all
59+
60+
return &target
61+
}
62+
63+
func validateTarget(logger utils.Logger, target *Target) bool {
64+
if target.ApiVersion != "shogun/v1" {
65+
logger.LogNewError("invalid apiVersion: %s", target.ApiVersion)
66+
return false
67+
}
68+
if target.Kind != TargetKind {
69+
logger.LogNewError("invalid kind: %s", target.Kind)
70+
return false
71+
}
72+
if target.Metadata.Type != ServerType && target.Metadata.Type != ClusterType {
73+
logger.LogNewError("invalid target type: %s", target.Metadata.Type)
74+
return false
75+
}
76+
if target.Spec.Host == "" || target.Spec.User == "" || target.Spec.AccessSecret == "" {
77+
logger.LogNewError("host, user, and access-key-secret must be provided")
78+
return false
79+
}
80+
if target.Spec.Port <= 0 || target.Spec.Port > 65535 {
81+
logger.LogNewError("invalid port number: %d", target.Spec.Port)
82+
return false
83+
}
84+
return true
85+
}

0 commit comments

Comments
 (0)