Skip to content

Commit

Permalink
SRE-5478 - Updating k8s-pipeliner for v2 provider (#50)
Browse files Browse the repository at this point in the history
* SRE-5478 - Updating k8s-pipeliner for v2 provider
  • Loading branch information
shraykay authored Jun 28, 2018
1 parent 7a9b09c commit 265cba2
Show file tree
Hide file tree
Showing 10 changed files with 492 additions and 16 deletions.
6 changes: 5 additions & 1 deletion cmd/k8s-pipeliner/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func main() {
Name: "linear, l",
Usage: "Assigns refs and reliesOn identifiers for you so you dont need to specify them. This is useful if your pipelines are always linear.",
},
cli.BoolFlag{
Name: "v2",
Usage: "Create your manifests with the v2 kubernetes provider",
},
},
},
{
Expand Down Expand Up @@ -65,7 +69,7 @@ func createAction(ctx *cli.Context) error {
return err
}

builder := builder.New(p, builder.WithLinear(ctx.Bool("linear")))
builder := builder.New(p, builder.WithV2Provider(ctx.Bool("v2")), builder.WithLinear(ctx.Bool("linear")))
return json.NewEncoder(os.Stdout).Encode(builder)
}

Expand Down
199 changes: 191 additions & 8 deletions pipeline/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,44 @@ import (
"encoding/json"
"errors"
"fmt"
"strings"

"github.com/namely/k8s-pipeliner/pipeline/builder/types"
"github.com/namely/k8s-pipeliner/pipeline/config"
corev1 "k8s.io/api/core/v1"
v1beta1 "k8s.io/api/extensions/v1beta1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var (
// ErrNoContainers is returned when a manifest has defined containers in it
ErrNoContainers = errors.New("builder: no containers were found in given manifest file")

// ErrNoDeployGroups is returned when a stage in the pipeline.yml does not have any deploy groups on it.
ErrNoDeployGroups = errors.New("builder: no deploy groups were defined in given pipeline.yml")
// ErrOverrideContention is returned when a manifest defines multiple containers and overrides were provided
ErrOverrideContention = errors.New("builder: overrides were provided to a group that has multiple containers defined")
// ErrDeploymentJob is returned when a manifest uses a deployment for a one shot job
ErrDeploymentJob = errors.New("builder: a deployment manifest was provided for a run job pod")
// ErrKubernetesAPI defines whether the manifest we've provided falls within the scope
ErrKubernetesAPI = errors.New("builder: could not marshal this type of kubernetes manifest")
)

const (
// JenkinsTrigger is the name of the type in the spinnaker json for pipeline config for jenkins job triggers
JenkinsTrigger = "jenkins"
// WebhookTrigger is the name of the type in the spinnaker json for pipeline config for webhooks
WebhookTrigger = "webhook"
// LoadBalancerFormat creates the label selectors to attach pipeline.yml labels to deployment selectors
LoadBalancerFormat = "load-balancer-%s"
)

// Builder constructs a spinnaker pipeline JSON from a pipeliner config
type Builder struct {
pipeline *config.Pipeline

isLinear bool
basePath string
isLinear bool
basePath string
v2Provider bool
}

// New initializes a new builder for a pipeline config
Expand All @@ -49,6 +61,7 @@ func (b *Builder) Pipeline() (*types.SpinnakerPipeline, error) {
LimitConcurrent: b.pipeline.DisableConcurrentExecutions,
KeepWaitingPipelines: b.pipeline.KeepQueuedPipelines,
Description: b.pipeline.Description,
AppConfig: map[string]interface{}{},
}

sp.Notifications = buildNotifications(b.pipeline.Notifications)
Expand Down Expand Up @@ -93,12 +106,22 @@ func (b *Builder) Pipeline() (*types.SpinnakerPipeline, error) {
var s types.Stage
var err error

if stage.RunJob != nil {
s, err = b.buildRunJobStage(stageIndex, stage)
}
if stage.Deploy != nil {
s, err = b.buildDeployStage(stageIndex, stage)
if b.v2Provider {
if stage.RunJob != nil {
s, err = b.buildV2RunJobStage(stageIndex, stage)
}
if stage.Deploy != nil {
s, err = b.buildV2ManifestStage(stageIndex, stage)
}
} else {
if stage.RunJob != nil {
s, err = b.buildRunJobStage(stageIndex, stage)
}
if stage.Deploy != nil {
s, err = b.buildDeployStage(stageIndex, stage)
}
}

if stage.ManualJudgement != nil {
s, err = b.buildManualJudgementStage(stageIndex, stage)
}
Expand Down Expand Up @@ -168,6 +191,108 @@ func (b *Builder) buildRunJobStage(index int, s config.Stage) (*types.RunJobStag
return rjs, nil
}

func (b *Builder) buildV2ManifestStage(index int, s config.Stage) (*types.ManifestStage, error) {
ds := &types.ManifestStage{
StageMetadata: buildStageMetadata(s, "deployManifest", index, b.isLinear),
Account: s.Account,
CloudProvider: "kubernetes",
Location: "",
ManifestArtifactAccount: "embedded-artifact",
ManifestName: "",
Moniker: types.Moniker{
App: b.pipeline.Application,
},
Relationships: types.Relationships{LoadBalancers: []interface{}{}, SecurityGroups: []interface{}{}},
Source: "text",
}

if len(s.Deploy.Groups) == 0 {
return nil, ErrNoDeployGroups
}

cluster := []string{s.Account, b.pipeline.Application, s.Deploy.Groups[0].Details, s.Deploy.Groups[0].Stack}

ds.Moniker.Cluster = strings.Join(cluster, "-")
ds.Moniker.Detail = s.Deploy.Groups[0].Details
ds.Moniker.Stack = s.Deploy.Groups[0].Stack

parser := NewManfifestParser(b.pipeline, b.basePath)

for _, group := range s.Deploy.Groups {
manifest, err := parser.ManifestFromScaffold(group)

if err != nil {
return nil, err
}

switch t := manifest.(type) {
case *v1beta1.Deployment:
_, err = buildDeployment(t, group)
if err != nil {
return nil, err
}
default:
return nil, ErrKubernetesAPI
}

j, err := json.Marshal(manifest)

if err != nil {
return nil, err
}
ds.Manifests = append(ds.Manifests, json.RawMessage(j))
}

return ds, nil
}

func (b *Builder) buildV2RunJobStage(index int, s config.Stage) (*types.ManifestStage, error) {
ds := &types.ManifestStage{
StageMetadata: buildStageMetadata(s, "deployManifest", index, b.isLinear),
Account: s.Account,
CloudProvider: "kubernetes",
Location: "",
ManifestArtifactAccount: "embedded-artifact",
ManifestName: "",
Moniker: types.Moniker{
App: b.pipeline.Application,
Cluster: fmt.Sprintf("%s-%s", b.pipeline.Application, s.Account),
Detail: "",
Stack: "",
},
Relationships: types.Relationships{LoadBalancers: []interface{}{}, SecurityGroups: []interface{}{}},
Source: "text",
}

parser := NewManfifestParser(b.pipeline, b.basePath)
obj, err := parser.ManifestFromScaffold(s.RunJob)

if err != nil {
return nil, err
}

switch t := obj.(type) {
case *corev1.Pod:
if s.RunJob.Container != nil {
t.Spec.Containers[0].Command = s.RunJob.Container.Command
t.Spec.Containers[0].Args = s.RunJob.Container.Args
}
default:
return nil, ErrDeploymentJob
}

j, err := json.Marshal(obj)

if err != nil {
return nil, err
}

ds.Manifests = append(ds.Manifests, json.RawMessage(j))

return ds, nil

}

// As of right now, this tool only supports deploying one server group at a time from a
// manifest file. So the clusters array will ALWAYS be 1 in length.
func (b *Builder) buildDeployStage(index int, s config.Stage) (*types.DeployStage, error) {
Expand Down Expand Up @@ -299,6 +424,64 @@ func buildNotifications(notifications []config.Notification) []types.Notificatio
return nots
}

func buildDeployment(deploy *v1beta1.Deployment, group config.Group) (*v1beta1.Deployment, error) {

if len(deploy.Spec.Template.Spec.Containers) == 0 {
return nil, ErrNoContainers
}

if overrides := group.ContainerOverrides; overrides != nil {
if len(deploy.Spec.Template.Spec.Containers) > 1 {
return nil, ErrOverrideContention
}
if overrides.Args != nil {
deploy.Spec.Template.Spec.Containers[0].Args = overrides.Args
}
if overrides.Command != nil {
deploy.Spec.Template.Spec.Containers[0].Command = overrides.Command
}
}

if lb := group.LoadBalancers; lb != nil {
labels := make(map[string]string)

for _, l := range lb {
labels[fmt.Sprintf(LoadBalancerFormat, l)] = "true"
}

if deploy.Spec.Selector != nil {
for key, val := range labels {
deploy.Spec.Selector.MatchLabels[key] = val
}
} else {
deploy.Spec.Selector = &metav1.LabelSelector{
MatchLabels: labels,
}
}

l := deploy.ObjectMeta.GetLabels()

if l == nil {
l = make(map[string]string)
}

for key, val := range labels {
l[key] = val
}

deploy.ObjectMeta.SetLabels(l)

l = deploy.Spec.Template.GetLabels()

for key, val := range labels {
l[key] = val
}

deploy.Spec.Template.SetLabels(l)
}
return deploy, nil
}

func newDefaultTrue(original *bool) bool {
if original == nil {
return true
Expand Down
89 changes: 89 additions & 0 deletions pipeline/builder/builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestBuilderAssignsPipelineConfiguration(t *testing.T) {
func TestBuilderPipelineStages(t *testing.T) {
wd, _ := os.Getwd()
file := filepath.Join(wd, "testdata", "deployment.full.yml")
podfile := filepath.Join(wd, "testdata", "podspec.yml")

t.Run("Triggers", func(t *testing.T) {
t.Run("Defaults to an empty slice", func(t *testing.T) {
Expand Down Expand Up @@ -203,6 +204,34 @@ func TestBuilderPipelineStages(t *testing.T) {
assert.Equal(t, expected, spinnaker.Stages[0].(*types.DeployStage).Clusters[0].PodAnnotations)
})

t.Run("V2 - Clusters are assigned", func(t *testing.T) {
pipeline := &config.Pipeline{
Stages: []config.Stage{
{
Name: "Test V2 Deploy Stage",
Deploy: &config.DeployStage{
Groups: []config.Group{
{
ManifestFile: file,
ContainerOverrides: &config.ContainerOverrides{
Command: []string{"cat", "dog"},
Args: []string{"mouse"},
},
LoadBalancers: []string{"lb1", "lb2"},
},
},
},
},
},
}

builder := builder.New(pipeline, builder.WithLinear(true), builder.WithV2Provider(true))
spinnaker, err := builder.Pipeline()
require.NoError(t, err, "error generating pipeline json")
assert.Equal(t, "Test V2 Deploy Stage", spinnaker.Stages[0].(*types.ManifestStage).Name)

})

t.Run("RequisiteStageRefIds defaults to an empty slice", func(t *testing.T) {
pipeline := &config.Pipeline{
Stages: []config.Stage{
Expand Down Expand Up @@ -257,6 +286,32 @@ func TestBuilderPipelineStages(t *testing.T) {
assert.Equal(t, "Test RunJob Stage", spinnaker.Stages[0].(*types.RunJobStage).Name)
})

t.Run("RunJob V2 stage is parsed correctly", func(t *testing.T) {
t.Run("Name is assigned", func(t *testing.T) {
pipeline := &config.Pipeline{
Stages: []config.Stage{
{
Name: "Test V2 RunJob Stage",
RunJob: &config.RunJobStage{
ManifestFile: podfile,
Container: &config.Container{
Command: []string{"cat", "dog"},
Args: []string{"mouse"},
},
},
},
},
}

builder := builder.New(pipeline, builder.WithV2Provider(true))
spinnaker, err := builder.Pipeline()
require.NoError(t, err, "error generating pipeline json")
assert.Equal(t, "Test V2 RunJob Stage", spinnaker.Stages[0].(*types.ManifestStage).Name)

})
},
)

t.Run("RequisiteStageRefIds defaults to an empty slice", func(t *testing.T) {
pipeline := &config.Pipeline{
Stages: []config.Stage{
Expand Down Expand Up @@ -369,6 +424,40 @@ func TestBuilderPipelineStages(t *testing.T) {
assert.Equal(t, expected, spinnaker.Stages[0].(*types.RunJobStage).Annotations)
})

t.Run("Image Descriptions are assigned", func(t *testing.T) {
pipeline := &config.Pipeline{
Stages: []config.Stage{
{
Name: "Test Deploy Stage",
Deploy: &config.DeployStage{
Groups: []config.Group{
{
ManifestFile: file,
PodOverrides: &config.PodOverrides{
Annotations: map[string]string{"hello": "world"},
},
},
},
},
},
},
ImageDescriptions: []config.ImageDescription{
{
Name: "hcm",
Account: "namely-registry",
ImageID: "${ parameters.docker_image }",
Registry: "registry.namely.land",
Repository: "namely/namely",
Tag: "${ parameters.docker_tag }",
Organization: "namely",
},
},
}
builder := builder.New(pipeline)
_, err := builder.Pipeline()
require.NoError(t, err, "error generating pipeline json")
})

t.Run("ServiceAccountName is assigned", func(t *testing.T) {
pipeline := &config.Pipeline{
Stages: []config.Stage{
Expand Down
Loading

0 comments on commit 265cba2

Please sign in to comment.