Skip to content

Commit 795565a

Browse files
authored
Added support for GCP Cloud Run applications (#317)
1 parent 55aec6b commit 795565a

9 files changed

Lines changed: 246 additions & 13 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# 0.0.150 (Dec 19, 2025)
2+
* Added app support (`push`, `deploy`, `launch`, `run`, `logs`) for Cloud Run jobs.
3+
14
# 0.0.149 (Dec 15, 2025)
25
* Enable support for GCP service account impersonation when pushing GCP artifacts.
36

admin/all/providers.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"gopkg.in/nullstone-io/nullstone.v0/aws/beanstalk"
77
"gopkg.in/nullstone-io/nullstone.v0/aws/ec2"
88
"gopkg.in/nullstone-io/nullstone.v0/aws/ecs"
9+
"gopkg.in/nullstone-io/nullstone.v0/gcp/cloudrun"
910
"gopkg.in/nullstone-io/nullstone.v0/gcp/gke"
1011
)
1112

@@ -52,6 +53,13 @@ var (
5253
Platform: "k8s",
5354
Subplatform: "gke",
5455
}
56+
cloudRunContract = types.ModuleContractName{
57+
Category: string(types.CategoryApp),
58+
Subcategory: string(types.SubcategoryAppServerless),
59+
Provider: "gcp",
60+
Platform: "cloudrun",
61+
Subplatform: "",
62+
}
5563

5664
Providers = admin.Providers{
5765
ecsContract: admin.Provider{
@@ -78,5 +86,9 @@ var (
7886
NewStatuser: nil,
7987
NewRemoter: gke.NewRemoter,
8088
},
89+
cloudRunContract: admin.Provider{
90+
NewStatuser: nil,
91+
NewRemoter: cloudrun.NewRemoter,
92+
},
8193
}
8294
)

cmd/run.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ package cmd
33
import (
44
"context"
55
"fmt"
6+
"os"
7+
68
"github.com/nullstone-io/deployment-sdk/app"
79
"github.com/nullstone-io/deployment-sdk/logging"
810
"github.com/nullstone-io/deployment-sdk/outputs"
911
"github.com/urfave/cli/v2"
1012
"gopkg.in/nullstone-io/go-api-client.v0"
1113
"gopkg.in/nullstone-io/nullstone.v0/admin"
12-
"os"
1314
)
1415

1516
var Run = func(appProviders app.Providers, providers admin.Providers) *cli.Command {
@@ -52,7 +53,7 @@ var Run = func(appProviders app.Providers, providers admin.Providers) *cli.Comma
5253
return err
5354
}
5455
options := admin.RunOptions{
55-
Container: c.String("container"),
56+
Container: c.String(ContainerFlag.Name),
5657
Username: user.Name,
5758
LogStreamer: logStreamer,
5859
LogEmitter: app.NewWriterLogEmitter(os.Stdout),

gcp/cloudrun/job_runner.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package cloudrun
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"time"
8+
9+
"cloud.google.com/go/run/apiv2/runpb"
10+
"github.com/nullstone-io/deployment-sdk/app"
11+
"github.com/nullstone-io/deployment-sdk/gcp"
12+
"github.com/nullstone-io/deployment-sdk/gcp/cloudrun"
13+
"golang.org/x/sync/errgroup"
14+
"gopkg.in/nullstone-io/nullstone.v0/admin"
15+
)
16+
17+
type JobRunner struct {
18+
JobName string
19+
MainContainerName string
20+
Adminer gcp.ServiceAccount
21+
}
22+
23+
const (
24+
// Polling interval for checking job status
25+
defaultPollInterval = 2 * time.Second
26+
)
27+
28+
func (r JobRunner) Run(ctx context.Context, options admin.RunOptions, cmd []string) error {
29+
// Start the job execution
30+
exec, err := r.startJob(ctx, cmd)
31+
if err != nil {
32+
return fmt.Errorf("error running job: %w", err)
33+
}
34+
35+
eg, ctx := errgroup.WithContext(ctx)
36+
eg.Go(func() error {
37+
absoluteTime := time.Now()
38+
executionFilter := fmt.Sprintf("resource.labels.execution_id=%q", exec.Uid)
39+
logStreamOptions := app.LogStreamOptions{
40+
StartTime: &absoluteTime,
41+
WatchInterval: time.Duration(0), // this makes sure the log stream doesn't exist until the context is canceled
42+
Emitter: options.LogEmitter,
43+
Selector: &executionFilter,
44+
}
45+
if err := options.LogStreamer.Stream(ctx, logStreamOptions); err != nil {
46+
if errors.Is(err, context.Canceled) {
47+
return nil
48+
}
49+
return err
50+
}
51+
return nil
52+
})
53+
54+
// TODO: How do we report start failures that aren't reported to logs? (e.g. container pull error)
55+
56+
// Monitor the job execution
57+
eg.Go(func() error {
58+
return r.monitorExecution(ctx, exec)
59+
})
60+
61+
// Wait for all goroutines to complete
62+
return eg.Wait()
63+
}
64+
65+
func (r JobRunner) startJob(ctx context.Context, cmd []string) (*runpb.Execution, error) {
66+
// Create a client for Cloud Run Jobs
67+
client, err := cloudrun.NewJobsClient(ctx, r.Adminer)
68+
if err != nil {
69+
return nil, fmt.Errorf("error creating Cloud Run Jobs client: %w", err)
70+
}
71+
72+
// If a command is provided, use it as "args" in main container overrides
73+
req := &runpb.RunJobRequest{
74+
Name: r.JobName,
75+
Overrides: &runpb.RunJobRequest_Overrides{ContainerOverrides: make([]*runpb.RunJobRequest_Overrides_ContainerOverride, 0)},
76+
}
77+
if len(cmd) > 0 {
78+
req.Overrides = &runpb.RunJobRequest_Overrides{
79+
ContainerOverrides: []*runpb.RunJobRequest_Overrides_ContainerOverride{
80+
{
81+
Name: r.MainContainerName,
82+
Args: cmd,
83+
},
84+
},
85+
}
86+
}
87+
88+
op, err := client.RunJob(ctx, req)
89+
if err != nil {
90+
return nil, fmt.Errorf("error running job: %w", err)
91+
}
92+
if _, err := op.Wait(ctx); err != nil {
93+
return nil, fmt.Errorf("an error occurred waiting for job to start: %w", err)
94+
}
95+
return op.Metadata()
96+
}
97+
98+
// monitorExecution monitors the job execution until it completes
99+
func (r JobRunner) monitorExecution(ctx context.Context, exec *runpb.Execution) error {
100+
client, err := cloudrun.NewExecutionsClient(ctx, r.Adminer)
101+
if err != nil {
102+
return fmt.Errorf("error creating Cloud Run Executions client: %w", err)
103+
}
104+
105+
ticker := time.NewTicker(defaultPollInterval)
106+
defer ticker.Stop()
107+
108+
for {
109+
select {
110+
case <-ctx.Done():
111+
return ctx.Err()
112+
case <-ticker.C:
113+
updated, err := client.GetExecution(ctx, &runpb.GetExecutionRequest{Name: exec.Name})
114+
if err != nil {
115+
return fmt.Errorf("error getting job execution status: %w", err)
116+
}
117+
118+
if (updated.FailedCount + updated.SucceededCount) >= updated.TaskCount {
119+
// Job execution completed
120+
if updated.FailedCount == 1 && updated.TaskCount == 1 {
121+
return fmt.Errorf("job execution failed")
122+
}
123+
if updated.FailedCount > 1 {
124+
return fmt.Errorf("%d of %d job executions failed", updated.FailedCount, updated.TaskCount)
125+
}
126+
return nil
127+
}
128+
}
129+
}
130+
}

gcp/cloudrun/outputs.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package cloudrun
2+
3+
import (
4+
"github.com/nullstone-io/deployment-sdk/gcp"
5+
"github.com/nullstone-io/deployment-sdk/gcp/creds"
6+
"github.com/nullstone-io/deployment-sdk/outputs"
7+
nstypes "gopkg.in/nullstone-io/go-api-client.v0/types"
8+
)
9+
10+
type Outputs struct {
11+
ServiceName string `ns:"service_name"`
12+
Deployer gcp.ServiceAccount `ns:"deployer"`
13+
MainContainerName string `ns:"main_container_name,optional"`
14+
JobName string `ns:"job_name,optional"`
15+
}
16+
17+
func (o *Outputs) InitializeCreds(source outputs.RetrieverSource, ws *nstypes.Workspace) {
18+
o.Deployer.RemoteTokenSourcer = creds.NewTokenSourcer(source, ws.StackId, ws.Uid, "deployer")
19+
}

gcp/cloudrun/remoter.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package cloudrun
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/nullstone-io/deployment-sdk/app"
8+
"github.com/nullstone-io/deployment-sdk/logging"
9+
"github.com/nullstone-io/deployment-sdk/outputs"
10+
"gopkg.in/nullstone-io/nullstone.v0/admin"
11+
)
12+
13+
var (
14+
_ admin.Remoter = Remoter{}
15+
)
16+
17+
func NewRemoter(ctx context.Context, osWriters logging.OsWriters, source outputs.RetrieverSource, appDetails app.Details) (admin.Remoter, error) {
18+
outs, err := outputs.Retrieve[Outputs](ctx, source, appDetails.Workspace, appDetails.WorkspaceConfig)
19+
if err != nil {
20+
return nil, err
21+
}
22+
outs.InitializeCreds(source, appDetails.Workspace)
23+
24+
return Remoter{
25+
OsWriters: osWriters,
26+
Details: appDetails,
27+
Infra: outs,
28+
}, nil
29+
}
30+
31+
type Remoter struct {
32+
OsWriters logging.OsWriters
33+
Details app.Details
34+
Infra Outputs
35+
}
36+
37+
func (r Remoter) Exec(ctx context.Context, options admin.RemoteOptions, cmd []string) error {
38+
if r.Infra.ServiceName == "" {
39+
return fmt.Errorf("cannot `exec` unless you have a long-running service, use `run` for a job/task")
40+
}
41+
42+
// TODO: Implement
43+
return nil
44+
}
45+
46+
func (r Remoter) Ssh(ctx context.Context, options admin.RemoteOptions) error {
47+
if r.Infra.ServiceName == "" {
48+
return fmt.Errorf("cannot `ssh` unless you have a long-running service, use `run` for a job/task")
49+
}
50+
51+
// TODO: Implement
52+
return nil
53+
}
54+
55+
func (r Remoter) Run(ctx context.Context, options admin.RunOptions, cmd []string) error {
56+
if r.Infra.ServiceName != "" {
57+
return fmt.Errorf("cannot use `run` for a long-running service, use `exec` instead")
58+
}
59+
60+
runner := JobRunner{
61+
JobName: r.Infra.JobName,
62+
MainContainerName: r.Infra.MainContainerName,
63+
Adminer: r.Infra.Deployer,
64+
}
65+
return runner.Run(ctx, options, cmd)
66+
}

gcp/gke/outputs.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package gke
22

33
import (
4-
"github.com/nullstone-io/deployment-sdk/docker"
54
"github.com/nullstone-io/deployment-sdk/gcp"
65
"github.com/nullstone-io/deployment-sdk/gcp/creds"
76
"github.com/nullstone-io/deployment-sdk/gcp/gke"
@@ -13,7 +12,6 @@ import (
1312
type Outputs struct {
1413
ServiceNamespace string `ns:"service_namespace"`
1514
ServiceName string `ns:"service_name"`
16-
ImageRepoUrl docker.ImageUrl `ns:"image_repo_url,optional"`
1715
Deployer gcp.ServiceAccount `ns:"deployer"`
1816
MainContainerName string `ns:"main_container_name,optional"`
1917
// JobDefinitionName is only specified for a job/task

go.mod

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module gopkg.in/nullstone-io/nullstone.v0
33
go 1.24.0
44

55
require (
6+
cloud.google.com/go/run v1.8.1
67
github.com/aws/aws-sdk-go-v2 v1.39.0
78
github.com/aws/aws-sdk-go-v2/service/ecs v1.64.0
89
github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk v1.33.3
@@ -11,15 +12,15 @@ require (
1112
github.com/go-git/go-git/v5 v5.16.2
1213
github.com/gosuri/uilive v0.0.4
1314
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
14-
github.com/nullstone-io/deployment-sdk v0.0.0-20251215195325-ea2bf8df2345
15-
github.com/nullstone-io/iac v0.0.0-20250922172952-38ed4863e8dc
15+
github.com/nullstone-io/deployment-sdk v0.0.0-20251219131441-89af0b8ea5de
16+
github.com/nullstone-io/iac v0.0.0-20251022110736-dc1cb12c5af7
1617
github.com/nullstone-io/module v0.2.10
1718
github.com/ryanuber/columnize v2.1.2+incompatible
1819
github.com/stretchr/testify v1.11.1
1920
github.com/urfave/cli/v2 v2.27.7
2021
golang.org/x/crypto v0.46.0
2122
golang.org/x/sync v0.19.0
22-
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20251105201937-c571247eeee2
23+
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20251209224316-3f2fb93be0ec
2324
k8s.io/api v0.34.0
2425
k8s.io/apimachinery v0.34.0
2526
k8s.io/client-go v0.34.0
@@ -73,6 +74,7 @@ require (
7374
cloud.google.com/go/compute v1.32.0 // indirect
7475
cloud.google.com/go/compute/metadata v0.6.0 // indirect
7576
cloud.google.com/go/iam v1.3.1 // indirect
77+
cloud.google.com/go/logging v1.13.0 // indirect
7678
cloud.google.com/go/longrunning v0.6.4 // indirect
7779
cloud.google.com/go/monitoring v1.23.0 // indirect
7880
cloud.google.com/go/storage v1.47.0 // indirect

go.sum

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ cloud.google.com/go/longrunning v0.6.4 h1:3tyw9rO3E2XVXzSApn1gyEEnH2K9SynNQjMlBi
1818
cloud.google.com/go/longrunning v0.6.4/go.mod h1:ttZpLCe6e7EXvn9OxpBRx7kZEB0efv8yBO6YnVMfhJs=
1919
cloud.google.com/go/monitoring v1.23.0 h1:M3nXww2gn9oZ/qWN2bZ35CjolnVHM3qnSbu6srCPgjk=
2020
cloud.google.com/go/monitoring v1.23.0/go.mod h1:034NnlQPDzrQ64G2Gavhl0LUHZs9H3rRmhtnp7jiJgg=
21+
cloud.google.com/go/run v1.8.1 h1:aeVLygw0BGLH+Zbj8v3K3nEHvKlgoq+j8fcRJaYZtxY=
22+
cloud.google.com/go/run v1.8.1/go.mod h1:wR5IG8Nujk9pyyNai187K4p8jzSLeqCKCAFBrZ2Sd4c=
2123
cloud.google.com/go/storage v1.47.0 h1:ajqgt30fnOMmLfWfu1PWcb+V9Dxz6n+9WKjdNg5R4HM=
2224
cloud.google.com/go/storage v1.47.0/go.mod h1:Ks0vP374w0PW6jOUameJbapbQKXqkjGd/OJRp2fb9IQ=
2325
cloud.google.com/go/trace v1.11.3 h1:c+I4YFjxRQjvAhRmSsmjpASUKq88chOX854ied0K/pE=
@@ -431,10 +433,10 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
431433
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
432434
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
433435
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
434-
github.com/nullstone-io/deployment-sdk v0.0.0-20251215195325-ea2bf8df2345 h1:+MTl4qHFTkg7VG35IwfLYkKrD8yhSPhAY0YClaMvrbQ=
435-
github.com/nullstone-io/deployment-sdk v0.0.0-20251215195325-ea2bf8df2345/go.mod h1:ga5kT0jT0B2IlgpAEqgAVCaRGCvtXBDeBekdMk6xqWE=
436-
github.com/nullstone-io/iac v0.0.0-20250922172952-38ed4863e8dc h1:0H6s6XS0BOHf6S/iwztiUKga6T9H4RzWx456FfWCUY8=
437-
github.com/nullstone-io/iac v0.0.0-20250922172952-38ed4863e8dc/go.mod h1:oh2Z/+5jIoBlnUGxPouxh86Avl+5gH2RVKb+q3vEve0=
436+
github.com/nullstone-io/deployment-sdk v0.0.0-20251219131441-89af0b8ea5de h1:K+GT+RGWYsqqH2LU2Llqeo3mEw//dSz7rShsLGi+VJI=
437+
github.com/nullstone-io/deployment-sdk v0.0.0-20251219131441-89af0b8ea5de/go.mod h1:yAEylocqYmmfgm6bgAE2na/GzUcIgnsKB9ThuNgJftA=
438+
github.com/nullstone-io/iac v0.0.0-20251022110736-dc1cb12c5af7 h1:lV5sOwb82P11EtBWo52tL/VCawU0i42DZ1Cq9tP/rnA=
439+
github.com/nullstone-io/iac v0.0.0-20251022110736-dc1cb12c5af7/go.mod h1:Sys7eNFaMQNel6K/+pVmzhMZYxX83PupRCMAVajLelw=
438440
github.com/nullstone-io/module v0.2.10 h1:wCKrlyxyH9XQW5HliW/V6qNsDgUQxUCcWL60Ojlz+2U=
439441
github.com/nullstone-io/module v0.2.10/go.mod h1:btQiO0giVWDvvaQ7CLnPmuPPakJc55lAr8OlE1LK6hg=
440442
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@@ -744,8 +746,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy
744746
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
745747
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
746748
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
747-
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20251105201937-c571247eeee2 h1:yLbco/NW9rBr2hCj0XK1cISNhQbThvcSdcaWSvqMWh8=
748-
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20251105201937-c571247eeee2/go.mod h1:Bj01hknD135uWb7j+9EIcE775ZXYv9pWSXdryTHMrhM=
749+
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20251209224316-3f2fb93be0ec h1:e5/Wu1FJ59Ovmutlfb71n2OimaTWcNCMb+EgxgFK4HQ=
750+
gopkg.in/nullstone-io/go-api-client.v0 v0.0.0-20251209224316-3f2fb93be0ec/go.mod h1:Bj01hknD135uWb7j+9EIcE775ZXYv9pWSXdryTHMrhM=
749751
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1 h1:d4KQkxAaAiRY2h5Zqis161Pv91A37uZyJOx73duwUwM=
750752
gopkg.in/rethinkdb/rethinkdb-go.v6 v6.2.1/go.mod h1:WbjuEoo1oadwzQ4apSDU+JTvmllEHtsNHS6y7vFc7iw=
751753
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=

0 commit comments

Comments
 (0)