Skip to content

Commit 7a8f054

Browse files
authored
hooks: add hooks execution environment variables (GoogleContainerTools#6163)
* hooks: add hooks execution environment variables * prefix env variables with `SKAFFOLD_` * address pr feedback
1 parent 1b94a38 commit 7a8f054

File tree

5 files changed

+287
-0
lines changed

5 files changed

+287
-0
lines changed

cmd/skaffold/app/cmd/runner.go

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
2828
sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/errors"
2929
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/event"
30+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/hooks"
3031
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/initializer"
3132
initConfig "github.com/GoogleContainerTools/skaffold/pkg/skaffold/initializer/config"
3233
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
@@ -69,6 +70,7 @@ func createNewRunner(out io.Writer, opts config.SkaffoldOptions) (runner.Runner,
6970
v1Configs = append(v1Configs, c.(*latestV1.SkaffoldConfig))
7071
}
7172
instrumentation.Init(v1Configs, opts.User)
73+
hooks.SetupStaticEnvOptions(runCtx)
7274
runner, err := v1.NewForConfig(runCtx)
7375
if err != nil {
7476
event.InititializationFailed(err)

pkg/skaffold/hooks/env.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
Copyright 2021 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hooks
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
"strings"
23+
"unicode"
24+
)
25+
26+
var staticEnvOpts StaticEnvOpts
27+
28+
// StaticEnvOpts contains the environment variables to be set in a lifecycle hook executor that don't change during the lifetime of the process.
29+
type StaticEnvOpts struct {
30+
DefaultRepo *string
31+
RPCPort int
32+
HTTPPort int
33+
WorkDir string
34+
}
35+
36+
// BuildEnvOpts contains the environment variables to be set in a build type lifecycle hook executor.
37+
type BuildEnvOpts struct {
38+
Image string
39+
PushImage bool
40+
ImageRepo string
41+
ImageTag string
42+
BuildContext string
43+
}
44+
45+
// SyncEnvOpts contains the environment variables to be set in a sync type lifecycle hook executor.
46+
type SyncEnvOpts struct {
47+
Image string
48+
BuildContext string
49+
FilesAddedOrModified *string
50+
FilesDeleted *string
51+
KubeContext string
52+
Namespaces string
53+
}
54+
55+
// DeployEnvOpts contains the environment variables to be set in a deploy type lifecycle hook executor.
56+
type DeployEnvOpts struct {
57+
RunID string
58+
KubeContext string
59+
Namespaces string
60+
}
61+
62+
type Config interface {
63+
DefaultRepo() *string
64+
GetWorkingDir() string
65+
RPCPort() int
66+
RPCHTTPPort() int
67+
}
68+
69+
func SetupStaticEnvOptions(cfg Config) {
70+
staticEnvOpts = StaticEnvOpts{
71+
DefaultRepo: cfg.DefaultRepo(),
72+
WorkDir: cfg.GetWorkingDir(),
73+
RPCPort: cfg.RPCPort(),
74+
HTTPPort: cfg.RPCHTTPPort(),
75+
}
76+
}
77+
78+
// getEnv converts the fields of BuildEnvOpts, SyncEnvOpts, DeployEnvOpts and CommonEnvOpts structs to a `key=value` environment variables slice.
79+
// Each field name is converted from CamelCase to SCREAMING_SNAKE_CASE and prefixed with `SKAFFOLD`.
80+
// For example the field `KubeContext` with value `kind` becomes `SKAFFOLD_KUBE_CONTEXT=kind`
81+
func getEnv(optsStruct interface{}) []string {
82+
var env []string
83+
structVal := reflect.ValueOf(optsStruct)
84+
t := structVal.Type()
85+
for i := 0; i < t.NumField(); i++ {
86+
f := t.Field(i)
87+
v := structVal.Field(i)
88+
if v.Kind() == reflect.Ptr && v.IsNil() {
89+
continue
90+
}
91+
v = reflect.Indirect(v)
92+
env = append(env, fmt.Sprintf("SKAFFOLD_%s=%v", toScreamingSnakeCase(f.Name), v.Interface()))
93+
}
94+
return env
95+
}
96+
97+
// toScreamingSnakeCase converts CamelCase strings to SCREAMING_SNAKE_CASE.
98+
// For example KubeContext to KUBE_CONTEXT
99+
func toScreamingSnakeCase(s string) string {
100+
r := []rune(s)
101+
var b strings.Builder
102+
for i := 0; i < len(r); i++ {
103+
if i > 0 && unicode.IsUpper(r[i]) {
104+
if !unicode.IsUpper(r[i-1]) {
105+
b.WriteRune('_')
106+
} else if i+1 < len(r) && !unicode.IsUpper(r[i+1]) {
107+
b.WriteRune('_')
108+
}
109+
}
110+
b.WriteRune(unicode.ToUpper(r[i]))
111+
}
112+
return b.String()
113+
}

pkg/skaffold/hooks/env_test.go

+162
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
Copyright 2021 The Skaffold Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package hooks
18+
19+
import (
20+
"testing"
21+
22+
"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
23+
"github.com/GoogleContainerTools/skaffold/testutil"
24+
)
25+
26+
func TestSetupStaticEnvOptions(t *testing.T) {
27+
defer func() {
28+
staticEnvOpts = StaticEnvOpts{}
29+
}()
30+
31+
cfg := mockCfg{
32+
defaultRepo: util.StringPtr("gcr.io/foo"),
33+
workDir: ".",
34+
rpcPort: 8080,
35+
httpPort: 8081,
36+
}
37+
SetupStaticEnvOptions(cfg)
38+
testutil.CheckDeepEqual(t, cfg.defaultRepo, staticEnvOpts.DefaultRepo)
39+
testutil.CheckDeepEqual(t, cfg.workDir, staticEnvOpts.WorkDir)
40+
testutil.CheckDeepEqual(t, cfg.rpcPort, staticEnvOpts.RPCPort)
41+
testutil.CheckDeepEqual(t, cfg.httpPort, staticEnvOpts.HTTPPort)
42+
}
43+
44+
func TestGetEnv(t *testing.T) {
45+
tests := []struct {
46+
description string
47+
input interface{}
48+
expected []string
49+
}{
50+
{
51+
description: "static env opts, all defined",
52+
input: StaticEnvOpts{
53+
DefaultRepo: util.StringPtr("gcr.io/foo"),
54+
RPCPort: 8080,
55+
HTTPPort: 8081,
56+
WorkDir: "./foo",
57+
},
58+
expected: []string{
59+
"SKAFFOLD_DEFAULT_REPO=gcr.io/foo",
60+
"SKAFFOLD_RPC_PORT=8080",
61+
"SKAFFOLD_HTTP_PORT=8081",
62+
"SKAFFOLD_WORK_DIR=./foo",
63+
},
64+
},
65+
{
66+
description: "static env opts, some missing",
67+
input: StaticEnvOpts{
68+
RPCPort: 8080,
69+
HTTPPort: 8081,
70+
WorkDir: "./foo",
71+
},
72+
expected: []string{
73+
"SKAFFOLD_RPC_PORT=8080",
74+
"SKAFFOLD_HTTP_PORT=8081",
75+
"SKAFFOLD_WORK_DIR=./foo",
76+
},
77+
},
78+
{
79+
description: "build env opts",
80+
input: BuildEnvOpts{
81+
Image: "foo",
82+
PushImage: true,
83+
ImageRepo: "gcr.io/foo",
84+
ImageTag: "latest",
85+
BuildContext: "./foo",
86+
},
87+
expected: []string{
88+
"SKAFFOLD_IMAGE=foo",
89+
"SKAFFOLD_PUSH_IMAGE=true",
90+
"SKAFFOLD_IMAGE_REPO=gcr.io/foo",
91+
"SKAFFOLD_IMAGE_TAG=latest",
92+
"SKAFFOLD_BUILD_CONTEXT=./foo",
93+
},
94+
},
95+
{
96+
description: "sync env opts, all defined",
97+
input: SyncEnvOpts{
98+
Image: "foo",
99+
FilesAddedOrModified: util.StringPtr("./foo/1;./foo/2"),
100+
FilesDeleted: util.StringPtr("./foo/3;./foo/4"),
101+
KubeContext: "minikube",
102+
Namespaces: "np1,np2,np3",
103+
BuildContext: "./foo",
104+
},
105+
expected: []string{
106+
"SKAFFOLD_IMAGE=foo",
107+
"SKAFFOLD_FILES_ADDED_OR_MODIFIED=./foo/1;./foo/2",
108+
"SKAFFOLD_FILES_DELETED=./foo/3;./foo/4",
109+
"SKAFFOLD_KUBE_CONTEXT=minikube",
110+
"SKAFFOLD_NAMESPACES=np1,np2,np3",
111+
"SKAFFOLD_BUILD_CONTEXT=./foo",
112+
},
113+
},
114+
{
115+
description: "sync env opts, some missing",
116+
input: SyncEnvOpts{
117+
Image: "foo",
118+
KubeContext: "minikube",
119+
Namespaces: "np1,np2,np3",
120+
BuildContext: "./foo",
121+
},
122+
expected: []string{
123+
"SKAFFOLD_IMAGE=foo",
124+
"SKAFFOLD_KUBE_CONTEXT=minikube",
125+
"SKAFFOLD_NAMESPACES=np1,np2,np3",
126+
"SKAFFOLD_BUILD_CONTEXT=./foo",
127+
},
128+
},
129+
{
130+
description: "deploy env opts",
131+
input: DeployEnvOpts{
132+
RunID: "1234",
133+
KubeContext: "minikube",
134+
Namespaces: "np1,np2,np3",
135+
},
136+
expected: []string{
137+
"SKAFFOLD_RUN_ID=1234",
138+
"SKAFFOLD_KUBE_CONTEXT=minikube",
139+
"SKAFFOLD_NAMESPACES=np1,np2,np3",
140+
},
141+
},
142+
}
143+
144+
for _, test := range tests {
145+
testutil.Run(t, test.description, func(t *testutil.T) {
146+
actual := getEnv(test.input)
147+
t.CheckElementsMatch(test.expected, actual)
148+
})
149+
}
150+
}
151+
152+
type mockCfg struct {
153+
defaultRepo *string
154+
workDir string
155+
rpcPort int
156+
httpPort int
157+
}
158+
159+
func (m mockCfg) DefaultRepo() *string { return m.defaultRepo }
160+
func (m mockCfg) GetWorkingDir() string { return m.workDir }
161+
func (m mockCfg) RPCPort() int { return m.rpcPort }
162+
func (m mockCfg) RPCHTTPPort() int { return m.httpPort }

pkg/skaffold/runner/runcontext/context.go

+2
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,8 @@ func (rc *RunContext) WatchPollInterval() int { return rc
199199
func (rc *RunContext) BuildConcurrency() int { return rc.Opts.BuildConcurrency }
200200
func (rc *RunContext) IsMultiConfig() bool { return rc.Pipelines.IsMultiPipeline() }
201201
func (rc *RunContext) GetRunID() string { return rc.RunID }
202+
func (rc *RunContext) RPCPort() int { return rc.Opts.RPCPort }
203+
func (rc *RunContext) RPCHTTPPort() int { return rc.Opts.RPCHTTPPort }
202204

203205
func GetRunContext(opts config.SkaffoldOptions, configs []schemaUtil.VersionedConfig) (*RunContext, error) {
204206
var pipelines []latestV1.Pipeline

testutil/util.go

+8
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ func (t *T) CheckError(shouldErr bool, err error) {
141141
CheckError(t.T, shouldErr, err)
142142
}
143143

144+
// CheckElementsMatch validates that two given slices contain the same elements
145+
// while disregarding their order.
146+
// Elements of both slices have to be comparable by '=='
147+
func (t *T) CheckElementsMatch(expected, actual interface{}) {
148+
t.Helper()
149+
CheckElementsMatch(t.T, expected, actual)
150+
}
151+
144152
// CheckErrorAndFailNow checks that the provided error complies with whether or not we expect an error
145153
// and fails the test execution immediately if it does not.
146154
// Useful for testing functions which return (obj interface{}, e error) and subsequent checks operate on `obj`

0 commit comments

Comments
 (0)