diff --git a/pkg/app/piped/controller/planner.go b/pkg/app/piped/controller/planner.go index 4e2baa75e0..fc6519eb8c 100644 --- a/pkg/app/piped/controller/planner.go +++ b/pkg/app/piped/controller/planner.go @@ -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, diff --git a/pkg/app/piped/controller/scheduler.go b/pkg/app/piped/controller/scheduler.go index a1bfa51b02..32e4e77fb1 100644 --- a/pkg/app/piped/controller/scheduler.go +++ b/pkg/app/piped/controller/scheduler.go @@ -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, diff --git a/pkg/app/piped/planner/kubernetes/kubernetes.go b/pkg/app/piped/planner/kubernetes/kubernetes.go index 8cb053c047..b415b630cb 100644 --- a/pkg/app/piped/planner/kubernetes/kubernetes.go +++ b/pkg/app/piped/planner/kubernetes/kubernetes.go @@ -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. @@ -480,3 +492,42 @@ 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) { + 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)) + 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 +} diff --git a/pkg/app/piped/planner/kubernetes/kubernetes_test.go b/pkg/app/piped/planner/kubernetes/kubernetes_test.go index 4ab81d9b08..8da6f6b4c0 100644 --- a/pkg/app/piped/planner/kubernetes/kubernetes_test.go +++ b/pkg/app/piped/planner/kubernetes/kubernetes_test.go @@ -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) { @@ -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 diff --git a/pkg/app/piped/planner/kubernetes/testdata/version_multi_workloads_same_image.yaml b/pkg/app/piped/planner/kubernetes/testdata/version_multi_workloads_same_image.yaml new file mode 100644 index 0000000000..21abdf39d5 --- /dev/null +++ b/pkg/app/piped/planner/kubernetes/testdata/version_multi_workloads_same_image.yaml @@ -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 diff --git a/pkg/app/piped/planner/planner.go b/pkg/app/piped/planner/planner.go index 076344e094..38d248e2dd 100644 --- a/pkg/app/piped/planner/planner.go +++ b/pkg/app/piped/planner/planner.go @@ -58,6 +58,7 @@ type Input struct { type Output struct { Version string + Versions []*model.ArtifactVersion SyncStrategy model.SyncStrategy Summary string Stages []*model.PipelineStage diff --git a/pkg/app/piped/trigger/deployment.go b/pkg/app/piped/trigger/deployment.go index ece84c3800..0e5a1a5584 100644 --- a/pkg/app/piped/trigger/deployment.go +++ b/pkg/app/piped/trigger/deployment.go @@ -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, }, diff --git a/pkg/app/server/grpcapi/piped_api.go b/pkg/app/server/grpcapi/piped_api.go index d9d00fe6e6..7a7e2793f6 100644 --- a/pkg/app/server/grpcapi/piped_api.go +++ b/pkg/app/server/grpcapi/piped_api.go @@ -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 @@ -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)) diff --git a/pkg/app/server/service/pipedservice/service.proto b/pkg/app/server/service/pipedservice/service.proto index 4a3ccee9df..64d57e9c3e 100644 --- a/pkg/app/server/service/pipedservice/service.proto +++ b/pkg/app/server/service/pipedservice/service.proto @@ -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; diff --git a/pkg/app/web/src/__fixtures__/dummy-deployment.ts b/pkg/app/web/src/__fixtures__/dummy-deployment.ts index 10235ba84f..692e105c84 100644 --- a/pkg/app/web/src/__fixtures__/dummy-deployment.ts +++ b/pkg/app/web/src/__fixtures__/dummy-deployment.ts @@ -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(), diff --git a/pkg/datastore/deploymentstore.go b/pkg/datastore/deploymentstore.go index 394664341b..c444828f26 100644 --- a/pkg/datastore/deploymentstore.go +++ b/pkg/datastore/deploymentstore.go @@ -70,7 +70,7 @@ 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 @@ -78,6 +78,7 @@ var ( d.RunningCommitHash = runningCommitHash d.RunningConfigFilename = runningConfigFilename d.Version = version + d.Versions = versions d.Stages = stages return nil } @@ -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 @@ -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) } diff --git a/pkg/datastore/deploymentstore_test.go b/pkg/datastore/deploymentstore_test.go index 5be2cacddd..c6e86ea26e 100644 --- a/pkg/datastore/deploymentstore_test.go +++ b/pkg/datastore/deploymentstore_test.go @@ -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", @@ -63,6 +71,7 @@ func TestDeploymentToPlannedUpdater(t *testing.T) { expectedRunningCommitHash, expectedRunningConfigFilename, expectedVersion, + expectedVersions, expectedStages, ) ) @@ -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) } diff --git a/pkg/model/application.proto b/pkg/model/application.proto index cf3c516d13..44bf9afe36 100644 --- a/pkg/model/application.proto +++ b/pkg/model/application.proto @@ -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]; diff --git a/pkg/model/common.proto b/pkg/model/common.proto index a2a03cf83e..c4fc5fc034 100644 --- a/pkg/model/common.proto +++ b/pkg/model/common.proto @@ -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; +} diff --git a/pkg/model/deployment.proto b/pkg/model/deployment.proto index e54be43174..596c483865 100644 --- a/pkg/model/deployment.proto +++ b/pkg/model/deployment.proto @@ -76,7 +76,9 @@ message Deployment { // e.g. Scale from 10 to 100 replicas. // e.g. Update image from v1.5.0 to v1.6.0. string summary = 22; + // TODO: Remove version from deployment model. string version = 23; + repeated ArtifactVersion versions = 24; // Hash value of the most recently successfully deployed commit. string running_commit_hash = 21;