Skip to content

Commit a8718ab

Browse files
authored
Merge pull request #295 from nais/workload-image
Add support for workload image option
2 parents 7e3c2dd + dc2fdde commit a8718ab

File tree

8 files changed

+105
-18
lines changed

8 files changed

+105
-18
lines changed

actions/deploy/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ The available configuration options for the NAIS deploy GitHub action.
2626
| VAR | | Comma-separated list of template variables in the form `key=value`. Will overwrite any identical template variable in the `VARS` file. |
2727
| VARS | `/dev/null` | File containing template variables. Will be interpolated with the `$RESOURCE` file. Must be JSON or YAML format. |
2828
| WAIT | `true` | Block until deployment has completed with either `success`, `failure` or `error` state. |
29+
| WORKLOAD_IMAGE | | Use this image in a companion Image resource. |
30+
| WORKLOAD_NAME | \(auto-detect\) | Name of workload. |
31+
2932

3033
Note that `OWNER` and `REPOSITORY` corresponds to the two parts of a full repository identifier.
3134
If that name is `navikt/myapplication`, those two variables should be set to `navikt` and `myapplication`, respectively.

actions/deploy/entrypoint.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ if [ -z "$DEPLOY_SERVER" ]; then
4949
echo
5050
echo ::endgroup::
5151
if [ $WGET_EXIT_CODE -eq 0 ]; then
52-
export DEPLOY_SERVER=$(jq --raw-output '.DEPLOY_SERVER' < deploy.json)
52+
export DEPLOY_SERVER=$(jq --raw-output '.DEPLOY_SERVER' < deploy.json)
5353
fi
5454
fi
5555

go.mod

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ require (
1414
github.com/lestrrat-go/jwx/v2 v2.1.4
1515
github.com/lib/pq v1.10.9
1616
github.com/nais/api/pkg/apiclient v0.0.0-20250203125351-77dc0579837a
17-
github.com/nais/liberator v0.0.0-20250212071940-b052d0557cca
17+
github.com/nais/liberator v0.0.0-20250318133902-16463bfb012c
1818
github.com/prometheus/client_golang v1.20.5
1919
github.com/sirupsen/logrus v1.9.3
2020
github.com/spf13/pflag v1.0.6
2121
github.com/spf13/viper v1.19.0
2222
github.com/stretchr/testify v1.10.0
23-
github.com/vektra/mockery/v2 v2.53.0
23+
github.com/vektra/mockery/v2 v2.53.2
2424
go.opentelemetry.io/otel v1.34.0
2525
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0
2626
go.opentelemetry.io/otel/sdk v1.34.0
@@ -121,11 +121,11 @@ require (
121121
go.opentelemetry.io/otel/metric v1.34.0 // indirect
122122
go.opentelemetry.io/proto/otlp v1.5.0 // indirect
123123
go.uber.org/multierr v1.11.0 // indirect
124-
golang.org/x/crypto v0.33.0 // indirect
124+
golang.org/x/crypto v0.35.0 // indirect
125125
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect
126126
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect
127127
golang.org/x/mod v0.23.0 // indirect
128-
golang.org/x/net v0.35.0 // indirect
128+
golang.org/x/net v0.36.0 // indirect
129129
golang.org/x/oauth2 v0.26.0 // indirect
130130
golang.org/x/sync v0.11.0 // indirect
131131
golang.org/x/sys v0.30.0 // indirect

go.sum

+8-8
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
230230
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
231231
github.com/nais/api/pkg/apiclient v0.0.0-20250203125351-77dc0579837a h1:0Xcuog4Mvhcr57aowsMLKsBnfB/DkVzb4LSc34Kf1Ik=
232232
github.com/nais/api/pkg/apiclient v0.0.0-20250203125351-77dc0579837a/go.mod h1:0Tle2/7Ub7oryydDjLbvppxs+GgxuRBV1C+7hIAPV+Y=
233-
github.com/nais/liberator v0.0.0-20250212071940-b052d0557cca h1:9Sgit939OTidcMbRmNw4Vd8OI4Bd9pVZ3XZuz7MZu7I=
234-
github.com/nais/liberator v0.0.0-20250212071940-b052d0557cca/go.mod h1:1IbhJBul6MCc66nwT38lKsKuuYTAFxmQs7pvPwnA0N8=
233+
github.com/nais/liberator v0.0.0-20250318133902-16463bfb012c h1:rX9eQ5Ie+mesMLZaYVo2YXPYI4RT0b42QS5UHFsVM0U=
234+
github.com/nais/liberator v0.0.0-20250318133902-16463bfb012c/go.mod h1:F3YcGoCG6HAyX5R2tgGH79/R0LBAU2xtRgRaveSXKiA=
235235
github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM=
236236
github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM=
237237
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
@@ -305,8 +305,8 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf
305305
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
306306
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
307307
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
308-
github.com/vektra/mockery/v2 v2.53.0 h1:QyZI8V2c+7oOEP/OnZjMMPlONXTgSsiLtaszzUz0yz0=
309-
github.com/vektra/mockery/v2 v2.53.0/go.mod h1:d0FVY5GgakIHczpYNVMLpYF2DttDgc97fHFTMcT3xlE=
308+
github.com/vektra/mockery/v2 v2.53.2 h1:4G/4fl9x722Yb8hLqH1YU3XZNRJFwl5KUMvpkmAyuC8=
309+
github.com/vektra/mockery/v2 v2.53.2/go.mod h1:UJT+mgXhCcOCHXTnM5cJHCZL+d76BYB+EbY1sFztEB8=
310310
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
311311
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
312312
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -357,8 +357,8 @@ golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWP
357357
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
358358
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
359359
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
360-
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
361-
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
360+
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
361+
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
362362
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs=
363363
golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo=
364364
golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ=
@@ -378,8 +378,8 @@ golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLL
378378
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
379379
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
380380
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
381-
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
382-
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
381+
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
382+
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
383383
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
384384
golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE=
385385
golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=

pkg/deployclient/config.go

+13-5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type Config struct {
2222
GithubToken string
2323
GrpcAuthentication bool
2424
GrpcUseTLS bool
25+
OpenTelemetryCollectorURL string
2526
Owner string
2627
PollInterval time.Duration
2728
PrintPayload bool
@@ -37,22 +38,24 @@ type Config struct {
3738
Telemetry *telemetry.PipelineTimings
3839
Timeout time.Duration
3940
TracingDashboardURL string
40-
OpenTelemetryCollectorURL string
4141
Variables []string
4242
VariablesFile string
4343
Wait bool
44+
WorkloadImage string
45+
WorkloadName string
4446
}
4547

4648
func InitConfig(cfg *Config) {
47-
flag.BoolVar(&cfg.Actions, "actions", getEnvBool("ACTIONS", false), "Use GitHub Actions compatible error and warning messages. (env ACTIONS)")
48-
flag.StringVar(&cfg.GithubToken, "github-token", os.Getenv("GITHUB_TOKEN"), "Github JWT. (env GITHUB_TOKEN)")
4949
flag.StringVar(&cfg.APIKey, "apikey", os.Getenv("APIKEY"), "NAIS Deploy API key. (env APIKEY)")
50+
flag.BoolVar(&cfg.Actions, "actions", getEnvBool("ACTIONS", false), "Use GitHub Actions compatible error and warning messages. (env ACTIONS)")
5051
flag.StringVar(&cfg.Cluster, "cluster", os.Getenv("CLUSTER"), "NAIS cluster to deploy into. (env CLUSTER)")
5152
flag.StringVar(&cfg.DeployServerURL, "deploy-server", getEnv("DEPLOY_SERVER", DefaultDeployServer), "URL to API server. (env DEPLOY_SERVER)")
5253
flag.BoolVar(&cfg.DryRun, "dry-run", getEnvBool("DRY_RUN", false), "Run templating, but don't actually make any requests. (env DRY_RUN)")
5354
flag.StringVar(&cfg.Environment, "environment", os.Getenv("ENVIRONMENT"), "Environment for GitHub deployment. Autodetected from nais.yaml if not specified. (env ENVIRONMENT)")
55+
flag.StringVar(&cfg.GithubToken, "github-token", os.Getenv("GITHUB_TOKEN"), "Github JWT. (env GITHUB_TOKEN)")
5456
flag.BoolVar(&cfg.GrpcAuthentication, "grpc-authentication", getEnvBool("GRPC_AUTHENTICATION", true), "Use team API key to authenticate requests. (env GRPC_AUTHENTICATION)")
5557
flag.BoolVar(&cfg.GrpcUseTLS, "grpc-use-tls", getEnvBool("GRPC_USE_TLS", true), "Use encrypted connection for gRPC calls. (env GRPC_USE_TLS)")
58+
flag.StringVar(&cfg.OpenTelemetryCollectorURL, "otel-collector-endpoint", getEnv("OTEL_COLLECTOR_ENDPOINT", DefaultOtelCollectorEndpoint), "OpenTelemetry collector endpoint. (env OTEL_COLLECTOR_ENDPOINT)")
5659
flag.StringVar(&cfg.Owner, "owner", getEnv("OWNER", DefaultOwner), "Owner of GitHub repository. (env OWNER)")
5760
flag.BoolVar(&cfg.PrintPayload, "print-payload", getEnvBool("PRINT_PAYLOAD", false), "Print templated resources to standard output. (env PRINT_PAYLOAD)")
5861
flag.BoolVar(&cfg.Quiet, "quiet", getEnvBool("QUIET", false), "Suppress printing of informational messages except errors. (env QUIET)")
@@ -61,14 +64,15 @@ func InitConfig(cfg *Config) {
6164
flag.StringSliceVar(&cfg.Resource, "resource", getEnvStringSlice("RESOURCE"), "File with Kubernetes resource. Can be specified multiple times. (env RESOURCE)")
6265
flag.BoolVar(&cfg.Retry, "retry", getEnvBool("RETRY", true), "Retry deploy when encountering transient errors. (env RETRY)")
6366
flag.StringVar(&cfg.Team, "team", os.Getenv("TEAM"), "Team making the deployment. Auto-detected from nais.yaml if possible. (env TEAM)")
64-
flag.StringVar(&cfg.OpenTelemetryCollectorURL, "otel-collector-endpoint", getEnv("OTEL_COLLECTOR_ENDPOINT", DefaultOtelCollectorEndpoint), "OpenTelemetry collector endpoint. (env OTEL_COLLECTOR_ENDPOINT)")
6567
flag.StringVar(&cfg.Traceparent, "traceparent", os.Getenv("TRACEPARENT"), "The W3C Trace Context traceparent value for the workflow run. (env TRACEPARENT)")
6668
flag.StringVar(&cfg.TelemetryInput, "telemetry", os.Getenv("TELEMETRY"), "Telemetry data from CI pipeline. (env TELEMETRY)")
6769
flag.DurationVar(&cfg.Timeout, "timeout", getEnvDuration("TIMEOUT", DefaultDeployTimeout), "Time to wait for successful deployment. (env TIMEOUT)")
6870
flag.StringVar(&cfg.TracingDashboardURL, "tracing-dashboard-url", getEnv("TRACING_DASHBOARD_URL", DefaultTracingDashboardURL), "Base URL to Grafana tracing dashboard onto which the trace ID can be appended (env TRACING_DASHBOARD_URL)")
6971
flag.StringSliceVar(&cfg.Variables, "var", getEnvStringSlice("VAR"), "Template variable in the form KEY=VALUE. Can be specified multiple times. (env VAR)")
7072
flag.StringVar(&cfg.VariablesFile, "vars", os.Getenv("VARS"), "File containing template variables. (env VARS)")
7173
flag.BoolVar(&cfg.Wait, "wait", getEnvBool("WAIT", false), "Block until deployment reaches final state (success, failure, error). (env WAIT)")
74+
flag.StringVar(&cfg.WorkloadImage, "workload-image", getEnv("WORKLOAD_IMAGE", ""), "Use this image in a companion Image resource. (env WORKLOAD_IMAGE)")
75+
flag.StringVar(&cfg.WorkloadName, "workload-name", os.Getenv("WORKLOAD_NAME"), "Name of workload if no resource specified. (env WORKLOAD_NAME)")
7276

7377
flag.Parse()
7478

@@ -123,10 +127,14 @@ func getEnvBool(key string, def bool) bool {
123127
}
124128

125129
func (cfg *Config) Validate() error {
126-
if len(cfg.Resource) == 0 {
130+
if len(cfg.Resource) == 0 && len(cfg.WorkloadName) == 0 {
127131
return ErrResourceRequired
128132
}
129133

134+
if len(cfg.WorkloadName) > 0 && len(cfg.WorkloadImage) == 0 {
135+
return ErrImageRequired
136+
}
137+
130138
if len(cfg.Cluster) == 0 {
131139
return ErrClusterRequired
132140
}

pkg/deployclient/deployclient.go

+30
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const (
3131

3232
var (
3333
ErrResourceRequired = errors.New("at least one Kubernetes resource is required to make sense of the deployment")
34+
ErrImageRequired = errors.New("workload-image is required when using workload-name")
3435
ErrAuthRequired = errors.New("Github token or API key required")
3536
ErrClusterRequired = errors.New("cluster required; see reference section in the documentation for available environments")
3637
ErrMalformedAPIKey = errors.New("API key must be a hex encoded string")
@@ -128,6 +129,35 @@ func Prepare(ctx context.Context, cfg *Config) (*pb.DeploymentRequest, error) {
128129
log.Infof("Detected environment '%s'", cfg.Environment)
129130
}
130131

132+
if len(cfg.WorkloadImage) > 0 {
133+
if len(cfg.WorkloadName) == 0 {
134+
log.Infof("Workload image specified, but not workload name; attempting auto-detection...")
135+
136+
workloadNames := make([]string, 0, len(resources))
137+
for i := range resources {
138+
workloadNames = append(workloadNames, detectWorkloadName(resources[i]))
139+
}
140+
141+
if len(workloadNames) == 1 {
142+
cfg.WorkloadName = workloadNames[0]
143+
log.Infof("Detected workload name '%s'", cfg.WorkloadName)
144+
} else if len(workloadNames) > 1 {
145+
log.Warnf("Multiple workload names detected, skipping image resource generation: %v", workloadNames)
146+
} else {
147+
log.Warn("No workload name detected, skipping image resource generation")
148+
}
149+
}
150+
151+
if len(cfg.WorkloadName) > 0 {
152+
log.Infof("Building image resource for workload %q with image %q", cfg.WorkloadName, cfg.WorkloadImage)
153+
resource, err := buildImageResource(cfg.WorkloadName, cfg.WorkloadImage, cfg.Team)
154+
if err != nil {
155+
return nil, ErrorWrap(ExitInternalError, fmt.Errorf("build image resource: %w", err))
156+
}
157+
resources = append(resources, resource)
158+
}
159+
}
160+
131161
for i := range resources {
132162
resources[i], err = InjectAnnotations(resources[i], BuildEnvironmentAnnotations())
133163
if err != nil {

pkg/deployclient/injection.go

+24
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package deployclient
33
import (
44
"encoding/json"
55
"fmt"
6+
nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1"
67
"os"
78
"strings"
89

@@ -84,6 +85,29 @@ func BuildEnvironmentAnnotations() map[string]string {
8485
return a
8586
}
8687

88+
func buildImageResource(workloadName, workloadImage, namespace string) (json.RawMessage, error) {
89+
image := nais_io_v1.Image{
90+
TypeMeta: v1.TypeMeta{
91+
Kind: "Image",
92+
APIVersion: "nais.io/v1",
93+
},
94+
ObjectMeta: v1.ObjectMeta{
95+
Name: workloadName,
96+
Namespace: namespace,
97+
},
98+
Spec: nais_io_v1.ImageSpec{
99+
Image: workloadImage,
100+
},
101+
}
102+
103+
encoded, err := json.Marshal(image)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
return encoded, nil
109+
}
110+
87111
func changeCause(annotations map[string]string) string {
88112
var commit, url string
89113
var ok bool

pkg/deployclient/template.go

+22
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,28 @@ func detectNamespace(resource json.RawMessage) string {
8787
return buf.Metadata.Namespace
8888
}
8989

90+
func detectWorkloadName(message json.RawMessage) string {
91+
type resource struct {
92+
ApiVersion string `json:"apiVersion"`
93+
Kind string `json:"kind"`
94+
Metadata struct {
95+
Name string `json:"name"`
96+
}
97+
}
98+
buf := &resource{}
99+
err := json.Unmarshal(message, buf)
100+
if err != nil {
101+
return ""
102+
}
103+
104+
if strings.HasPrefix(buf.ApiVersion, "nais.io") {
105+
if buf.Kind == "Application" || buf.Kind == "Naisjob" {
106+
return buf.Metadata.Name
107+
}
108+
}
109+
return ""
110+
}
111+
90112
// Wrap JSON resources in a JSON array.
91113
func wrapResources(resources []json.RawMessage) (json.RawMessage, error) {
92114
return json.Marshal(resources)

0 commit comments

Comments
 (0)