diff --git a/pkg/app/piped/executor/cloudrun/cloudrun.go b/pkg/app/piped/executor/cloudrun/cloudrun.go index dc354df8fa..e7936c7b6a 100644 --- a/pkg/app/piped/executor/cloudrun/cloudrun.go +++ b/pkg/app/piped/executor/cloudrun/cloudrun.go @@ -106,12 +106,52 @@ func configureServiceManifest(sm provider.ServiceManifest, revision string, traf lp.Info("Successfully prepared service manifest with traffic percentages as below:") for _, t := range traffics { - lp.Infof(" %s: %d", t.RevisionName, t.Percent) + if t.Tag != "" { + lp.Infof(" %s: %d%% (tag: %s)", t.RevisionName, t.Percent, t.Tag) + } else { + lp.Infof(" %s: %d", t.RevisionName, t.Percent) + } } return true } +func getExistingRevisionTags(ctx context.Context, client provider.Client, serviceName string, lp executor.LogPersister) map[string]string { + tags := make(map[string]string) + + service, err := client.Get(ctx, serviceName) + if err != nil { + if err == provider.ErrServiceNotFound { + lp.Info("Service does not exist yet, no tags to preserve") + return tags + } + lp.Errorf("Failed to fetch existing service for tag preservation: %v", err) + return tags + } + + if service.Status != nil && service.Status.Traffic != nil { + for _, target := range service.Status.Traffic { + if target.Tag != "" && target.RevisionName != "" { + tags[target.RevisionName] = target.Tag + lp.Infof("Found existing tag '%s' for revision %s", target.Tag, target.RevisionName) + } + } + } + + return tags +} + +func mergeTrafficWithExistingTags(traffics []provider.RevisionTraffic, existingTags map[string]string) []provider.RevisionTraffic { + result := make([]provider.RevisionTraffic, len(traffics)) + for i, t := range traffics { + result[i] = t + if tag, exists := existingTags[t.RevisionName]; exists && t.Tag == "" { + result[i].Tag = tag + } + } + return result +} + func apply(ctx context.Context, client provider.Client, sm provider.ServiceManifest, lp executor.LogPersister) bool { lp.Info("Start applying the service manifest") diff --git a/pkg/app/piped/executor/cloudrun/deploy.go b/pkg/app/piped/executor/cloudrun/deploy.go index 7bbadc9656..83952d36a0 100644 --- a/pkg/app/piped/executor/cloudrun/deploy.go +++ b/pkg/app/piped/executor/cloudrun/deploy.go @@ -99,12 +99,17 @@ func (e *deployExecutor) ensureSync(ctx context.Context) model.StageStatus { return model.StageStatus_STAGE_FAILURE } + existingTags := getExistingRevisionTags(ctx, e.client, sm.Name, e.LogPersister) + traffics := []provider.RevisionTraffic{ { RevisionName: revision, Percent: 100, }, } + + traffics = mergeTrafficWithExistingTags(traffics, existingTags) + if !configureServiceManifest(sm, revision, traffics, e.LogPersister) { return model.StageStatus_STAGE_FAILURE } @@ -185,6 +190,8 @@ func (e *deployExecutor) ensurePromote(ctx context.Context) model.StageStatus { return model.StageStatus_STAGE_FAILURE } + existingTags := getExistingRevisionTags(ctx, e.client, sm.Name, e.LogPersister) + traffics := []provider.RevisionTraffic{ { RevisionName: revision, @@ -196,6 +203,8 @@ func (e *deployExecutor) ensurePromote(ctx context.Context) model.StageStatus { }, } + traffics = mergeTrafficWithExistingTags(traffics, existingTags) + exist, err := revisionExists(ctx, e.client, revision, e.LogPersister) if err != nil { return model.StageStatus_STAGE_FAILURE diff --git a/pkg/app/piped/executor/cloudrun/rollback.go b/pkg/app/piped/executor/cloudrun/rollback.go index 19582e3b1e..58e42ea0a2 100644 --- a/pkg/app/piped/executor/cloudrun/rollback.go +++ b/pkg/app/piped/executor/cloudrun/rollback.go @@ -87,12 +87,17 @@ func (e *rollbackExecutor) ensureRollback(ctx context.Context) model.StageStatus return model.StageStatus_STAGE_FAILURE } + existingTags := getExistingRevisionTags(ctx, e.client, sm.Name, e.LogPersister) + traffics := []provider.RevisionTraffic{ { RevisionName: revision, Percent: 100, }, } + + traffics = mergeTrafficWithExistingTags(traffics, existingTags) + if !configureServiceManifest(sm, revision, traffics, e.LogPersister) { return model.StageStatus_STAGE_FAILURE } diff --git a/pkg/app/piped/executor/cloudrun/tag_preservation_test.go b/pkg/app/piped/executor/cloudrun/tag_preservation_test.go new file mode 100644 index 0000000000..be72f96ac5 --- /dev/null +++ b/pkg/app/piped/executor/cloudrun/tag_preservation_test.go @@ -0,0 +1,152 @@ +// Copyright 2024 The PipeCD Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cloudrun + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "google.golang.org/api/run/v1" + + provider "github.com/pipe-cd/pipecd/pkg/app/piped/platformprovider/cloudrun" +) + +type mockClient struct { + service *provider.Service + getErr error +} + +func (m *mockClient) Create(ctx context.Context, sm provider.ServiceManifest) (*provider.Service, error) { + return nil, nil +} + +func (m *mockClient) Update(ctx context.Context, sm provider.ServiceManifest) (*provider.Service, error) { + return nil, nil +} + +func (m *mockClient) Get(ctx context.Context, serviceName string) (*provider.Service, error) { + if m.getErr != nil { + return nil, m.getErr + } + return m.service, nil +} + +func (m *mockClient) List(ctx context.Context, options *provider.ListOptions) ([]*provider.Service, string, error) { + return nil, "", nil +} + +func (m *mockClient) GetRevision(ctx context.Context, name string) (*provider.Revision, error) { + return nil, nil +} + +func (m *mockClient) ListRevisions(ctx context.Context, options *provider.ListRevisionsOptions) ([]*provider.Revision, string, error) { + return nil, "", nil +} + +type mockLogPersister struct { + logs []string +} + +func (m *mockLogPersister) Info(msg string) { + m.logs = append(m.logs, "INFO: "+msg) +} + +func (m *mockLogPersister) Infof(format string, args ...interface{}) { + m.logs = append(m.logs, "INFO: "+format) +} + +func (m *mockLogPersister) Success(msg string) { + m.logs = append(m.logs, "SUCCESS: "+msg) +} + +func (m *mockLogPersister) Successf(format string, args ...interface{}) { + m.logs = append(m.logs, "SUCCESS: "+format) +} + +func (m *mockLogPersister) Error(msg string) { + m.logs = append(m.logs, "ERROR: "+msg) +} + +func (m *mockLogPersister) Errorf(format string, args ...interface{}) { + m.logs = append(m.logs, "ERROR: "+format) +} + +func (m *mockLogPersister) Write(data []byte) (int, error) { + return len(data), nil +} + +func TestGetExistingRevisionTags(t *testing.T) { + ctx := context.Background() + client := &mockClient{ + service: &provider.Service{ + Status: &run.ServiceStatus{ + Traffic: []*run.TrafficTarget{ + { + RevisionName: "revision-1", + Percent: 80, + Tag: "stable", + }, + { + RevisionName: "revision-2", + Percent: 20, + Tag: "canary", + }, + { + RevisionName: "revision-3", + Percent: 0, + }, + }, + }, + }, + } + lp := &mockLogPersister{} + + tags := getExistingRevisionTags(ctx, client, "test-service", lp) + + assert.Equal(t, 2, len(tags)) + assert.Equal(t, "stable", tags["revision-1"]) + assert.Equal(t, "canary", tags["revision-2"]) + assert.NotContains(t, tags, "revision-3") +} + +func TestMergeTrafficWithExistingTags(t *testing.T) { + traffics := []provider.RevisionTraffic{ + { + RevisionName: "revision-1", + Percent: 50, + }, + { + RevisionName: "revision-2", + Percent: 50, + Tag: "explicit-tag", + }, + { + RevisionName: "new-revision", + Percent: 0, + }, + } + existingTags := map[string]string{ + "revision-1": "stable", + "revision-2": "old-tag", + } + + result := mergeTrafficWithExistingTags(traffics, existingTags) + + assert.Equal(t, 3, len(result)) + assert.Equal(t, "stable", result[0].Tag, "Should preserve existing tag") + assert.Equal(t, "explicit-tag", result[1].Tag, "Should keep explicit tag, not override") + assert.Equal(t, "", result[2].Tag, "New revision should have no tag") +} diff --git a/pkg/app/piped/platformprovider/cloudrun/client.go b/pkg/app/piped/platformprovider/cloudrun/client.go index 9d8f8ae74f..1356ffebfa 100644 --- a/pkg/app/piped/platformprovider/cloudrun/client.go +++ b/pkg/app/piped/platformprovider/cloudrun/client.go @@ -107,6 +107,24 @@ func (c *client) Update(ctx context.Context, sm ServiceManifest) (*Service, erro return (*Service)(service), nil } +func (c *client) Get(ctx context.Context, serviceName string) (*Service, error) { + var ( + svc = run.NewNamespacesServicesService(c.client) + name = makeCloudRunServiceName(c.projectID, serviceName) + call = svc.Get(name) + ) + call.Context(ctx) + + service, err := call.Do() + if err != nil { + if e, ok := err.(*googleapi.Error); ok && e.Code == http.StatusNotFound { + return nil, ErrServiceNotFound + } + return nil, err + } + return (*Service)(service), nil +} + func (c *client) List(ctx context.Context, options *ListOptions) ([]*Service, string, error) { var ( svc = run.NewNamespacesServicesService(c.client) diff --git a/pkg/app/piped/platformprovider/cloudrun/cloudrun.go b/pkg/app/piped/platformprovider/cloudrun/cloudrun.go index 7574e8f724..7d4fc9bd5a 100644 --- a/pkg/app/piped/platformprovider/cloudrun/cloudrun.go +++ b/pkg/app/piped/platformprovider/cloudrun/cloudrun.go @@ -95,6 +95,7 @@ const ( type Client interface { Create(ctx context.Context, sm ServiceManifest) (*Service, error) Update(ctx context.Context, sm ServiceManifest) (*Service, error) + Get(ctx context.Context, serviceName string) (*Service, error) List(ctx context.Context, options *ListOptions) ([]*Service, string, error) GetRevision(ctx context.Context, name string) (*Revision, error) ListRevisions(ctx context.Context, options *ListRevisionsOptions) ([]*Revision, string, error) diff --git a/pkg/app/piped/platformprovider/cloudrun/servicemanifest.go b/pkg/app/piped/platformprovider/cloudrun/servicemanifest.go index cb3af939da..a6fd83b5c5 100644 --- a/pkg/app/piped/platformprovider/cloudrun/servicemanifest.go +++ b/pkg/app/piped/platformprovider/cloudrun/servicemanifest.go @@ -39,6 +39,7 @@ func (m ServiceManifest) SetRevision(name string) error { type RevisionTraffic struct { RevisionName string `json:"revisionName"` Percent int `json:"percent"` + Tag string `json:"tag,omitempty"` } func (m ServiceManifest) UpdateTraffic(revisions []RevisionTraffic) error {