Skip to content
1 change: 1 addition & 0 deletions pkg/app/piped/controller/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ func (p *planner) reportDeploymentPlanned(ctx context.Context, out pln.Output) e
RunningCommitHash: p.lastSuccessfulCommitHash,
RunningConfigFilename: p.lastSuccessfulConfigFilename,
Version: out.Version,
Versions: out.Versions,
Stages: out.Stages,
DeploymentChainId: p.deployment.DeploymentChainId,
DeploymentChainBlockIndex: p.deployment.DeploymentChainBlockIndex,
Expand Down
1 change: 1 addition & 0 deletions pkg/app/piped/controller/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -679,6 +679,7 @@ func (s *scheduler) reportMostRecentlySuccessfulDeployment(ctx context.Context)
Trigger: s.deployment.Trigger,
Summary: s.deployment.Summary,
Version: s.deployment.Version,
Versions: s.deployment.Versions,
ConfigFilename: s.deployment.GitPath.GetApplicationConfigFilename(),
StartedAt: s.deployment.CreatedAt,
CompletedAt: s.deployment.CompletedAt,
Expand Down
52 changes: 52 additions & 0 deletions pkg/app/piped/planner/kubernetes/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,18 @@ func (p *Planner) Plan(ctx context.Context, in planner.Input) (out planner.Outpu
out.Version = version
}

if versions, e := determineVersions(newManifests); e != nil || len(versions) == 0 {
in.Logger.Error("unable to determine versions", zap.Error(e))
out.Versions = []*model.ArtifactVersion{
{
Kind: model.ArtifactVersion_UNKNOWN,
Version: versionUnknown,
},
}
} else {
out.Versions = versions
}

autoRollback := *cfg.Input.AutoRollback

// In case the strategy has been decided by trigger.
Expand Down Expand Up @@ -480,3 +492,43 @@ func determineVersion(manifests []provider.Manifest) (string, error) {

return b.String(), nil
}

// determineVersions decides artifact versions of an application.
// It finds all container images that are being specified in the workload manifests then returns their names, version numbers, and urls.
func determineVersions(manifests []provider.Manifest) ([]*model.ArtifactVersion, error) {
versions := []*model.ArtifactVersion{}
imageMap := map[string]struct{}{}

for _, m := range manifests {
// TODO: Determine container image version from other workload kinds such as StatefulSet, Pod, Daemon, CronJob...
if !m.Key.IsDeployment() {
continue
}
data, err := m.MarshalJSON()
if err != nil {
return nil, err
}
var d resource.Deployment
if err := json.Unmarshal(data, &d); err != nil {
return nil, err
}

containers := d.Spec.Template.Spec.Containers
// remove duplicate images on multiple manifests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// remove duplicate images on multiple manifests
// Remove duplicate images on multiple manifests.

As our convention, comments start with a capital character and finish with a dot.

for _, c := range containers {
imageMap[c.Image] = struct{}{}
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move initialization versions slice to later to know its size.

Suggested change
versions := []*model.ArtifactVersion{}
imageMap := map[string]struct{}{}
for _, m := range manifests {
// TODO: Determine container image version from other workload kinds such as StatefulSet, Pod, Daemon, CronJob...
if !m.Key.IsDeployment() {
continue
}
data, err := m.MarshalJSON()
if err != nil {
return nil, err
}
var d resource.Deployment
if err := json.Unmarshal(data, &d); err != nil {
return nil, err
}
containers := d.Spec.Template.Spec.Containers
// remove duplicate images on multiple manifests
for _, c := range containers {
imageMap[c.Image] = struct{}{}
}
}
imageMap := map[string]struct{}{}
for _, m := range manifests {
// TODO: Determine container image version from other workload kinds such as StatefulSet, Pod, Daemon, CronJob...
if !m.Key.IsDeployment() {
continue
}
data, err := m.MarshalJSON()
if err != nil {
return nil, err
}
var d resource.Deployment
if err := json.Unmarshal(data, &d); err != nil {
return nil, err
}
containers := d.Spec.Template.Spec.Containers
// remove duplicate images on multiple manifests
for _, c := range containers {
imageMap[c.Image] = struct{}{}
}
}
versions := make([]*model.ArtifactVersion, 0, len(imageMap))

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for suggestion!

for i := range imageMap {
image := parseContainerImage(i)
versions = append(versions, &model.ArtifactVersion{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: image.tag,
Name: image.name,
Url: i,
})
}

return versions, nil
}
87 changes: 87 additions & 0 deletions pkg/app/piped/planner/kubernetes/kubernetes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

provider "github.com/pipe-cd/pipecd/pkg/app/piped/cloudprovider/kubernetes"
"github.com/pipe-cd/pipecd/pkg/config"
"github.com/pipe-cd/pipecd/pkg/model"
)

func TestDecideStrategy(t *testing.T) {
Expand Down Expand Up @@ -438,6 +439,92 @@ func TestDetermineVersion(t *testing.T) {
}
}

func TestDetermineVersions(t *testing.T) {
testcases := []struct {
name string
manifests string
expected []*model.ArtifactVersion
expectedError error
}{
{
name: "no workload",
manifests: "testdata/version_no_workload.yaml",
expected: []*model.ArtifactVersion{},
},
{
name: "single container",
manifests: "testdata/version_single_container.yaml",
expected: []*model.ArtifactVersion{
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "v1.0.0",
Name: "helloworld",
Url: "gcr.io/pipecd/helloworld:v1.0.0",
},
},
},
{
name: "multiple containers",
manifests: "testdata/version_multi_containers.yaml",
expected: []*model.ArtifactVersion{
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "v1.0.0",
Name: "helloworld",
Url: "gcr.io/pipecd/helloworld:v1.0.0",
},
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "v0.6.0",
Name: "my-service",
Url: "gcr.io/pipecd/my-service:v0.6.0",
},
},
},
{
name: "multiple workloads",
manifests: "testdata/version_multi_workloads.yaml",
expected: []*model.ArtifactVersion{
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "v1.0.0",
Name: "helloworld",
Url: "gcr.io/pipecd/helloworld:v1.0.0",
},
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "v0.5.0",
Name: "my-service",
Url: "gcr.io/pipecd/my-service:v0.5.0",
},
},
},
{
name: "multiple workloads using same container image",
manifests: "testdata/version_multi_workloads_same_image.yaml",
expected: []*model.ArtifactVersion{
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "v1.0.0",
Name: "helloworld",
Url: "gcr.io/pipecd/helloworld:v1.0.0",
},
},
},
}

for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
manifests, err := provider.LoadManifestsFromYAMLFile(tc.manifests)
require.NoError(t, err)

versions, err := determineVersions(manifests)
assert.Equal(t, tc.expected, versions)
assert.Equal(t, tc.expectedError, err)
})
}
}

func TestCheckImageChange(t *testing.T) {
testcases := []struct {
name string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: simple
labels:
app: simple
pipecd.dev/managed-by: piped
spec:
replicas: 2
selector:
matchLabels:
app: simple
template:
metadata:
labels:
app: simple
spec:
containers:
- name: helloworld
image: gcr.io/pipecd/helloworld:v1.0.0
args:
- hello
- hi
ports:
- containerPort: 9085
---
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 9376
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-service
labels:
pipecd.dev/managed-by: piped
app: simple
spec:
replicas: 2
selector:
matchLabels:
app: simple
template:
metadata:
labels:
app: simple
spec:
containers:
- name: helloworld
image: gcr.io/pipecd/helloworld:v1.0.0
args:
- hi
- hello
ports:
- containerPort: 9085
1 change: 1 addition & 0 deletions pkg/app/piped/planner/planner.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type Input struct {

type Output struct {
Version string
Versions []*model.ArtifactVersion
SyncStrategy model.SyncStrategy
Summary string
Stages []*model.PipelineStage
Expand Down
1 change: 1 addition & 0 deletions pkg/app/piped/trigger/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ func reportMostRecentlyTriggeredDeployment(ctx context.Context, client apiClient
Trigger: d.Trigger,
Summary: d.Summary,
Version: d.Version,
Versions: d.Versions,
StartedAt: d.CreatedAt,
CompletedAt: d.CompletedAt,
},
Expand Down
3 changes: 2 additions & 1 deletion pkg/app/server/grpcapi/piped_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ type pipedApiDeploymentStore interface {
Add(ctx context.Context, app *model.Deployment) error
Get(ctx context.Context, id string) (*model.Deployment, error)
List(ctx context.Context, opts datastore.ListOptions) ([]*model.Deployment, string, error)
UpdateToPlanned(ctx context.Context, id, summary, reason, runningCommitHash, runningConfigFilename, version string, stages []*model.PipelineStage) error
UpdateToPlanned(ctx context.Context, id, summary, reason, runningCommitHash, runningConfigFilename, version string, versions []*model.ArtifactVersion, stages []*model.PipelineStage) error
UpdateToCompleted(ctx context.Context, id string, status model.DeploymentStatus, stageStatuses map[string]model.StageStatus, reason string, completedAt int64) error
UpdateStatus(ctx context.Context, id string, status model.DeploymentStatus, reason string) error
UpdateStageStatus(ctx context.Context, id, stageID string, status model.StageStatus, reason string, requires []string, visible bool, retriedCount int32, completedAt int64) error
Expand Down Expand Up @@ -403,6 +403,7 @@ func (a *PipedAPI) ReportDeploymentPlanned(ctx context.Context, req *pipedservic
req.RunningCommitHash,
req.RunningConfigFilename,
req.Version,
req.Versions,
req.Stages,
); err != nil {
return nil, gRPCEntityOperationError(err, fmt.Sprintf("update deployment %s as planned", req.DeploymentId))
Expand Down
1 change: 1 addition & 0 deletions pkg/app/server/service/pipedservice/service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ message ReportDeploymentPlannedRequest {
string running_config_filename = 9;
// The application version this deployment is trying to deploy.
string version = 5;
repeated model.ArtifactVersion versions = 10;
// The planned stages.
// Empty means nothing has changed compared to when the deployment was created.
repeated model.PipelineStage stages = 6;
Expand Down
1 change: 1 addition & 0 deletions pkg/app/web/src/__fixtures__/dummy-deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const dummyDeployment: Deployment.AsObject = {
statusReason: "good",
trigger: dummyTrigger,
version: "0.0.0",
versionsList: [],
cloudProvider: "kube-1",
labelsMap: [],
createdAt: createdAt.unix(),
Expand Down
9 changes: 5 additions & 4 deletions pkg/datastore/deploymentstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,15 @@ func (d *deploymentCollection) Encode(e interface{}) (map[Shard][]byte, error) {
}

var (
toPlannedUpdateFunc = func(summary, statusReason, runningCommitHash, runningConfigFilename, version string, stages []*model.PipelineStage) func(*model.Deployment) error {
toPlannedUpdateFunc = func(summary, statusReason, runningCommitHash, runningConfigFilename, version string, versions []*model.ArtifactVersion, stages []*model.PipelineStage) func(*model.Deployment) error {
return func(d *model.Deployment) error {
d.Status = model.DeploymentStatus_DEPLOYMENT_PLANNED
d.Summary = summary
d.StatusReason = statusReason
d.RunningCommitHash = runningCommitHash
d.RunningConfigFilename = runningConfigFilename
d.Version = version
d.Versions = versions
d.Stages = stages
return nil
}
Expand Down Expand Up @@ -134,7 +135,7 @@ type DeploymentStore interface {
Add(ctx context.Context, d *model.Deployment) error
Get(ctx context.Context, id string) (*model.Deployment, error)
List(ctx context.Context, opts ListOptions) ([]*model.Deployment, string, error)
UpdateToPlanned(ctx context.Context, id, summary, reason, runningCommitHash, runningConfigFilename, version string, stages []*model.PipelineStage) error
UpdateToPlanned(ctx context.Context, id, summary, reason, runningCommitHash, runningConfigFilename, version string, versions []*model.ArtifactVersion, stages []*model.PipelineStage) error
UpdateToCompleted(ctx context.Context, id string, status model.DeploymentStatus, stageStatuses map[string]model.StageStatus, reason string, completedAt int64) error
UpdateStatus(ctx context.Context, id string, status model.DeploymentStatus, reason string) error
UpdateStageStatus(ctx context.Context, id, stageID string, status model.StageStatus, reason string, requires []string, visible bool, retriedCount int32, completedAt int64) error
Expand Down Expand Up @@ -222,8 +223,8 @@ func (s *deploymentStore) update(ctx context.Context, id string, updater func(*m
})
}

func (s *deploymentStore) UpdateToPlanned(ctx context.Context, id, summary, reason, runningCommitHash, runningConfigFilename, version string, stages []*model.PipelineStage) error {
updater := toPlannedUpdateFunc(summary, reason, runningCommitHash, runningConfigFilename, version, stages)
func (s *deploymentStore) UpdateToPlanned(ctx context.Context, id, summary, reason, runningCommitHash, runningConfigFilename, version string, versions []*model.ArtifactVersion, stages []*model.PipelineStage) error {
updater := toPlannedUpdateFunc(summary, reason, runningCommitHash, runningConfigFilename, version, versions, stages)
return s.update(ctx, id, updater)
}

Expand Down
12 changes: 11 additions & 1 deletion pkg/datastore/deploymentstore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ func TestDeploymentToPlannedUpdater(t *testing.T) {
expectedRunningCommitHash = "update-running-commit-hash"
expectedRunningConfigFilename = "update-running-config-filename"
expectedVersion = "update-version"
expectedStages = []*model.PipelineStage{
expectedVersions = []*model.ArtifactVersion{
{
Kind: model.ArtifactVersion_CONTAINER_IMAGE,
Version: "update-version",
Name: "update-image-name",
Url: "dummy-registry/update-image-name:update-version",
},
}
expectedStages = []*model.PipelineStage{
{
Id: "stage-id1",
Name: "stage1",
Expand Down Expand Up @@ -63,6 +71,7 @@ func TestDeploymentToPlannedUpdater(t *testing.T) {
expectedRunningCommitHash,
expectedRunningConfigFilename,
expectedVersion,
expectedVersions,
expectedStages,
)
)
Expand All @@ -75,6 +84,7 @@ func TestDeploymentToPlannedUpdater(t *testing.T) {
assert.Equal(t, expectedRunningCommitHash, d.RunningCommitHash)
assert.Equal(t, expectedRunningConfigFilename, d.RunningConfigFilename)
assert.Equal(t, expectedVersion, d.Version)
assert.Equal(t, expectedVersions, d.Versions)
assert.Equal(t, expectedStages, d.Stages)
}

Expand Down
12 changes: 0 additions & 12 deletions pkg/model/application.proto
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,6 @@ message ApplicationSyncState {
int64 timestamp = 5 [(validate.rules).int64.gt = 0];
}

message ArtifactVersion {
enum Kind {
UNKNOWN = 0;
CONTAINER_IMAGE = 1;
}

Kind kind = 1 [(validate.rules).enum.defined_only = true];
string version = 2 [(validate.rules).string.min_len = 1];
string name = 3;
string url = 4;
}

message ApplicationDeploymentReference {
string deployment_id = 1 [(validate.rules).string.min_len = 1];
DeploymentTrigger trigger = 2 [(validate.rules).message.required = true];
Expand Down
12 changes: 12 additions & 0 deletions pkg/model/common.proto
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,15 @@ message ApplicationInfo {
// This field will be no longer needed as labels can be an alternative.
string env_name = 14 [deprecated=true];
}

message ArtifactVersion {
enum Kind {
UNKNOWN = 0;
CONTAINER_IMAGE = 1;
}

Kind kind = 1 [(validate.rules).enum.defined_only = true];
string version = 2 [(validate.rules).string.min_len = 1];
string name = 3;
string url = 4;
}
Loading