Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions pkg/app/pipedv1/plugin/cloudrun/deployment/determine.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2026 The PipeCD Authors.
package deployment

import (
"context"
"fmt"
"path/filepath"
"strings"

sdk "github.com/pipe-cd/piped-plugin-sdk-go"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/cloudrunservice/config"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/cloudrunservice/provider"
)

func determineVersions(ctx context.Context, input *sdk.DetermineVersionsInput[config.CloudRunApplicationSpec]) (*sdk.DetermineVersionsResponse, error) {
manifestPath := "service.yaml"
if input.Request.DeploymentSource.ApplicationConfig != nil && input.Request.DeploymentSource.ApplicationConfig.Spec != nil && input.Request.DeploymentSource.ApplicationConfig.Spec.Input.ServiceManifestFile != "" {
manifestPath = input.Request.DeploymentSource.ApplicationConfig.Spec.Input.ServiceManifestFile
}
path := filepath.Join(input.Request.DeploymentSource.ApplicationDirectory, manifestPath)

sm, err := provider.LoadServiceManifest(path)
if err != nil {
return nil, fmt.Errorf("failed to load service manifest: %w", err)
}

images, err := sm.ExtractImages()
if err != nil {
return nil, fmt.Errorf("failed to extract images: %w", err)
}

versions := make([]sdk.ArtifactVersion, 0, len(images))
for _, image := range images {
name, tag := parseContainerImage(image)
versions = append(versions, sdk.ArtifactVersion{
Version: tag,
Name: name,
URL: image,
})
}

return &sdk.DetermineVersionsResponse{
Versions: versions,
}, nil
}

func parseContainerImage(image string) (name, tag string) {
lastColon := strings.LastIndex(image, ":")
lastSlash := strings.LastIndex(image, "/")

if lastColon > lastSlash {
tag = image[lastColon+1:]
imageWithoutTag := image[:lastColon]
paths := strings.Split(imageWithoutTag, "/")
name = paths[len(paths)-1]
} else {
paths := strings.Split(image, "/")
name = paths[len(paths)-1]
tag = "latest"
}
return
}

func determineStrategy(ctx context.Context, input *sdk.DetermineStrategyInput[config.CloudRunApplicationSpec]) (*sdk.DetermineStrategyResponse, error) {
if input.Request.RunningDeploymentSource.ApplicationDirectory == "" {
return &sdk.DetermineStrategyResponse{
Strategy: sdk.SyncStrategyQuickSync,
Summary: "First time deployment. Quick sync will be used",
}, nil
}

return &sdk.DetermineStrategyResponse{
Strategy: sdk.SyncStrategyPipelineSync,
Summary: "Updating existing deployment. Pipeline sync will be used",
}, nil
}
89 changes: 89 additions & 0 deletions pkg/app/pipedv1/plugin/cloudrun/deployment/determine_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright 2026 The PipeCD Authors.
package deployment

import (
"context"
"testing"

"github.com/stretchr/testify/assert"

sdk "github.com/pipe-cd/piped-plugin-sdk-go"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/cloudrunservice/config"
)

func TestParseContainerImage(t *testing.T) {
tests := []struct {
name string
image string
expectedName string
expectedTag string
}{
{
name: "typical image with tag",
image: "gcr.io/project/app:v1.0.0",
expectedName: "app",
expectedTag: "v1.0.0",
},
{
name: "image with registry port and tag",
image: "localhost:5000/app:v1.0.0",
expectedName: "app",
expectedTag: "v1.0.0",
},
{
name: "image without tag",
image: "ubuntu",
expectedName: "ubuntu",
expectedTag: "latest",
},
{
name: "image with registry port without tag",
image: "registry.local:8080/foo/bar",
expectedName: "bar",
expectedTag: "latest",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
name, tag := parseContainerImage(tc.image)
assert.Equal(t, tc.expectedName, name)
assert.Equal(t, tc.expectedTag, tag)
})
}
}

func TestDetermineStrategy(t *testing.T) {
tests := []struct {
name string
runningDeploymentSource string
expectedStrategy sdk.SyncStrategy
}{
{
name: "first time deployment",
runningDeploymentSource: "",
expectedStrategy: sdk.SyncStrategyQuickSync,
},
{
name: "subsequent deployment",
runningDeploymentSource: "/path/to/app",
expectedStrategy: sdk.SyncStrategyPipelineSync,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
input := &sdk.DetermineStrategyInput[config.CloudRunApplicationSpec]{
Request: sdk.DetermineStrategyRequest[config.CloudRunApplicationSpec]{
RunningDeploymentSource: sdk.DeploymentSource[config.CloudRunApplicationSpec]{
ApplicationDirectory: tc.runningDeploymentSource,
},
},
}

resp, err := determineStrategy(context.Background(), input)
assert.NoError(t, err)
assert.Equal(t, tc.expectedStrategy, resp.Strategy)
})
}
}
77 changes: 77 additions & 0 deletions pkg/app/pipedv1/plugin/cloudrun/deployment/execute_sync.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// Copyright 2026 The PipeCD Authors.
package deployment

import (
"context"
"fmt"
"path/filepath"
"time"

sdk "github.com/pipe-cd/piped-plugin-sdk-go"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/cloudrunservice/config"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/cloudrunservice/provider"
)

func executeSync(ctx context.Context, targets []*sdk.DeployTarget[config.CloudRunDeployTargetConfig], input *sdk.ExecuteStageInput[config.CloudRunApplicationSpec]) (*sdk.ExecuteStageResponse, error) {
if len(targets) == 0 {
return nil, fmt.Errorf("deploy target is not configured")
}
target := targets[0]

manifestPath := "service.yaml"
if input.Request.TargetDeploymentSource.ApplicationConfig != nil && input.Request.TargetDeploymentSource.ApplicationConfig.Spec != nil && input.Request.TargetDeploymentSource.ApplicationConfig.Spec.Input.ServiceManifestFile != "" {
manifestPath = input.Request.TargetDeploymentSource.ApplicationConfig.Spec.Input.ServiceManifestFile
}
path := filepath.Join(input.Request.TargetDeploymentSource.ApplicationDirectory, manifestPath)
sm, err := provider.LoadServiceManifest(path)
if err != nil {
return &sdk.ExecuteStageResponse{
Status: sdk.StageStatusFailure,
}, fmt.Errorf("failed to load service manifest: %w", err)
}

client, err := provider.NewClient(ctx, target.Config.Project, target.Config.Region, target.Config.CredentialsFile, input.Logger)
if err != nil {
return &sdk.ExecuteStageResponse{
Status: sdk.StageStatusFailure,
}, fmt.Errorf("failed to create cloud run client: %w", err)
}

commitHash := input.Request.TargetDeploymentSource.CommitHash
if len(commitHash) > 7 {
commitHash = commitHash[:7]
}
revisionName := fmt.Sprintf("%s-%s", sm.Name(), commitHash)

if err := sm.SetRevision(revisionName); err != nil {
return nil, fmt.Errorf("failed to set revision name: %w", err)
}
if err := sm.UpdateAllTraffic(revisionName); err != nil {
return nil, fmt.Errorf("failed to configure traffic mapping: %w", err)
}
sm.AddLabels(map[string]string{
"pipecd-dev-managed-by": "piped",
"pipecd-dev-application": input.Request.Deployment.ApplicationID,
})

input.Logger.Info(fmt.Sprintf("applying service manifest for %s...", sm.Name()))
_, err = client.GetService(ctx, sm.Name())
if err == provider.ErrServiceNotFound {
_, err = client.Create(ctx, sm)
} else if err == nil {
_, err = client.Update(ctx, sm)
}

if err != nil {
return &sdk.ExecuteStageResponse{
Status: sdk.StageStatusFailure,
}, fmt.Errorf("failed to apply service manifest: %w", err)
}

input.Logger.Info(fmt.Sprintf("waiting for revision %s to be ready...", revisionName))
time.Sleep(2 * time.Second)

return &sdk.ExecuteStageResponse{
Status: sdk.StageStatusSuccess,
}, nil
}
15 changes: 9 additions & 6 deletions pkg/app/pipedv1/plugin/cloudrun/deployment/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package deployment

import (
"context"
"fmt"
"slices"

sdk "github.com/pipe-cd/piped-plugin-sdk-go"
Expand Down Expand Up @@ -49,18 +50,20 @@ func (p *Plugin) BuildPipelineSyncStages(ctx context.Context, _ *sdk.ConfigNone,
}

func (p *Plugin) ExecuteStage(ctx context.Context, _ *sdk.ConfigNone, dts []*sdk.DeployTarget[config.CloudRunDeployTargetConfig], input *sdk.ExecuteStageInput[config.CloudRunApplicationSpec]) (*sdk.ExecuteStageResponse, error) {
// TODO implement me
panic("implement me")
switch input.Request.StageName {
case StageCloudRunSync:
return executeSync(ctx, dts, input)
default:
return nil, fmt.Errorf("unsupported stage: %s", input.Request.StageName)
}
}

func (p *Plugin) DetermineVersions(ctx context.Context, _ *sdk.ConfigNone, input *sdk.DetermineVersionsInput[config.CloudRunApplicationSpec]) (*sdk.DetermineVersionsResponse, error) {
// TODO implement me
panic("implement me")
return determineVersions(ctx, input)
}

func (p *Plugin) DetermineStrategy(ctx context.Context, _ *sdk.ConfigNone, input *sdk.DetermineStrategyInput[config.CloudRunApplicationSpec]) (*sdk.DetermineStrategyResponse, error) {
// TODO implement me
panic("implement me")
return determineStrategy(ctx, input)
}

func (p *Plugin) BuildQuickSyncStages(ctx context.Context, _ *sdk.ConfigNone, input *sdk.BuildQuickSyncStagesInput) (*sdk.BuildQuickSyncStagesResponse, error) {
Expand Down
14 changes: 14 additions & 0 deletions pkg/app/pipedv1/plugin/cloudrun/deployment/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
package deployment

import (
"context"
"testing"

sdk "github.com/pipe-cd/piped-plugin-sdk-go"
"github.com/pipe-cd/pipecd/pkg/app/pipedv1/plugin/cloudrunservice/config"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -161,3 +163,15 @@ func Test_buildPipelineStages(t *testing.T) {
})
}
}

func TestPlugin_ExecuteStage_Unsupported(t *testing.T) {
t.Parallel()
p := &Plugin{}
input := &sdk.ExecuteStageInput[config.CloudRunApplicationSpec]{
Request: sdk.ExecuteStageRequest[config.CloudRunApplicationSpec]{
StageName: "UNKNOWN_STAGE",
},
}
_, err := p.ExecuteStage(context.Background(), nil, nil, input)
assert.ErrorContains(t, err, "unsupported stage")
}
23 changes: 19 additions & 4 deletions pkg/app/pipedv1/plugin/cloudrun/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ toolchain go1.24.2
require (
github.com/pipe-cd/piped-plugin-sdk-go v0.0.0-20250813060314-58a44ff1d325
github.com/stretchr/testify v1.10.0
go.uber.org/zap v1.19.1
golang.org/x/oauth2 v0.30.0
google.golang.org/api v0.169.0
k8s.io/apimachinery v0.24.3
sigs.k8s.io/yaml v1.5.0
)

require (
Expand All @@ -19,19 +24,26 @@ require (
github.com/creasty/defaults v1.6.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/pprof v0.0.0-20221103000818-d260c55eee4c // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pipe-cd/pipecd v0.52.1-0.20250731104149-f611ce3501c5 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
Expand All @@ -42,26 +54,29 @@ require (
github.com/spf13/pflag v1.0.6 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
go.opentelemetry.io/otel v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.28.0 // indirect
go.opentelemetry.io/otel/trace v1.28.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.19.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.169.0 // indirect
google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.64.1 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
sigs.k8s.io/yaml v1.5.0 // indirect
k8s.io/klog/v2 v2.60.1 // indirect
k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect
sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect
)
Loading