Add Container object to environment build and runtime specs (#413)

Environment Specs so far had only an image URL to specify a container image.

This was fine for public images but fell short in a few of cases:
(a) Using private image registries
(b) Specifying environment variables (this is needed for workflows helm install)
(c) Setting a SecurityContext for the container

This change adds the Container object to both build and runtime Environments. 

Compatibility is preserved -- the existing ImageURL field is still used.  See the comments in types.go for the overriding rules in the case that both Container and ImageURL are specified.
This commit is contained in:
Erwin van Eyk
2018-03-22 00:13:02 -07:00
committed by Soam Vasani
parent c76b6bacc0
commit 70a93a7302
14 changed files with 363 additions and 43 deletions
+2 -2
View File
@@ -22,7 +22,7 @@ minikube, you'll need to set the proper environment variables with
``` ```
# Get dependencies # Get dependencies
$ glide install $ glide install -v
# Build fission server and an image # Build fission server and an image
$ pushd fission-bundle $ pushd fission-bundle
@@ -41,7 +41,7 @@ minikube and its built-in docker daemon:
Next, install fission with this image on your kubernetes cluster using the helm chart: Next, install fission with this image on your kubernetes cluster using the helm chart:
``` ```
$ helm install --set "image=minikube/fission-bundle,pullPolicy=IfNotPresent,analytics=false" charts/fission-all $ helm install --set "image=minikube/fission-bundle,imageTag=latest,pullPolicy=IfNotPresent,analytics=false" charts/fission-all
``` ```
And if you're changing the CLI too, you can build it with: And if you're changing the CLI too, you can build it with:
+3 -3
View File
@@ -15,7 +15,7 @@
.DEFAULT_GOAL := build .DEFAULT_GOAL := build
IMAGE ?= fission/fission-bundle IMAGE ?= fission/fission-bundle
VERSION ?= 0.3.0 VERSION ?= latest
ARCH ?= amd64 ARCH ?= amd64
OS ?= linux OS ?= linux
@@ -42,5 +42,5 @@ image-push: image
docker push "$(IMAGE):$(VERSION)" docker push "$(IMAGE):$(VERSION)"
clean: clean:
@rm -rf fission-bundle/fission-bundle @rm -f fission-bundle/fission-bundle
@rm -rf fission/fission @rm -f fission/fission
+2 -2
View File
@@ -497,7 +497,7 @@ func (envw *environmentWatcher) createBuilderDeployment(env *crd.Environment) (*
}, },
}, },
Containers: []apiv1.Container{ Containers: []apiv1.Container{
{ fission.MergeContainerSpecs(&apiv1.Container{
Name: "builder", Name: "builder",
Image: env.Spec.Builder.Image, Image: env.Spec.Builder.Image,
ImagePullPolicy: apiv1.PullAlways, ImagePullPolicy: apiv1.PullAlways,
@@ -530,7 +530,7 @@ func (envw *environmentWatcher) createBuilderDeployment(env *crd.Environment) (*
}, },
}, },
}, },
}, }, env.Spec.Builder.Container),
{ {
Name: "fetcher", Name: "fetcher",
Image: envw.fetcherImage, Image: envw.fetcherImage,
+21
View File
@@ -27,6 +27,8 @@ import (
"syscall" "syscall"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/imdario/mergo"
apiv1 "k8s.io/client-go/pkg/api/v1"
) )
func UrlForFunction(name string) string { func UrlForFunction(name string) string {
@@ -66,3 +68,22 @@ func LoggingMiddleware(next http.Handler) http.Handler {
} }
}) })
} }
// MergeContainerSpecs merges container specs using a predefined order.
//
// The order of the arguments indicates which spec has precedence (lower index takes precedence over higher indexes).
// Slices and maps are merged; other fields are set only if they are a zero value.
func MergeContainerSpecs(specs ...*apiv1.Container) apiv1.Container {
result := &apiv1.Container{}
for _, spec := range specs {
if spec == nil {
continue
}
err := mergo.Merge(result, spec)
if err != nil {
panic(err)
}
}
return *result
}
+4 -7
View File
@@ -23,6 +23,7 @@ import (
"log" "log"
"net/http" "net/http"
"os" "os"
"reflect"
"testing" "testing"
"time" "time"
@@ -214,11 +215,7 @@ func TestEnvironmentApi(t *testing.T) {
e, err := g.client.EnvironmentGet(m) e, err := g.client.EnvironmentGet(m)
panicIf(err) panicIf(err)
assert(testEnv.Spec.AllowedFunctionsPerContainer == e.Spec.AllowedFunctionsPerContainer, "env AllowedFunctionsPerContainer should match after reading") assert(reflect.DeepEqual(testEnv.Spec, e.Spec), "env should match after reading")
assert(testEnv.Spec.Poolsize == e.Spec.Poolsize, "env Poolsize should match after reading")
assert(testEnv.Spec.Builder == e.Spec.Builder, "env Builder should match after reading")
assert(testEnv.Spec.Runtime == e.Spec.Runtime, "env Runtime should match after reading")
assert(testEnv.Spec.Version == e.Spec.Version, "env Version should match after reading")
testEnv.Metadata.ResourceVersion = m.ResourceVersion testEnv.Metadata.ResourceVersion = m.ResourceVersion
testEnv.Spec.Runtime.Image = "another-img" testEnv.Spec.Runtime.Image = "another-img"
@@ -266,9 +263,9 @@ func TestWatchApi(t *testing.T) {
w, err := g.client.WatchGet(m) w, err := g.client.WatchGet(m)
panicIf(err) panicIf(err)
assert((testWatch.Spec.Namespace == w.Spec.Namespace && assert(testWatch.Spec.Namespace == w.Spec.Namespace &&
testWatch.Spec.Type == w.Spec.Type && testWatch.Spec.Type == w.Spec.Type &&
testWatch.Spec.FunctionReference == w.Spec.FunctionReference), "watch should match after reading") testWatch.Spec.FunctionReference == w.Spec.FunctionReference, "watch should match after reading")
testWatch.Metadata.Name = "yyy" testWatch.Metadata.Name = "yyy"
m2, err := g.client.WatchCreate(testWatch) m2, err := g.client.WatchCreate(testWatch)
+2 -2
View File
@@ -193,7 +193,7 @@ func (deploy *NewDeploy) getDeploymentSpec(fn *crd.Function, env *crd.Environmen
}, },
}, },
Containers: []apiv1.Container{ Containers: []apiv1.Container{
{ fission.MergeContainerSpecs(&apiv1.Container{
Name: fn.Metadata.Name, Name: fn.Metadata.Name,
Image: env.Spec.Runtime.Image, Image: env.Spec.Runtime.Image,
ImagePullPolicy: apiv1.PullIfNotPresent, ImagePullPolicy: apiv1.PullIfNotPresent,
@@ -223,7 +223,7 @@ func (deploy *NewDeploy) getDeploymentSpec(fn *crd.Function, env *crd.Environmen
}, },
}, },
Resources: resources, Resources: resources,
}, }, env.Spec.Runtime.Container),
{ {
Name: "fetcher", Name: "fetcher",
Image: deploy.fetcherImg, Image: deploy.fetcherImg,
+2 -2
View File
@@ -524,7 +524,7 @@ func (gp *GenericPool) createPool() error {
}, },
}, },
Containers: []apiv1.Container{ Containers: []apiv1.Container{
{ fission.MergeContainerSpecs(&apiv1.Container{
Name: gp.env.Metadata.Name, Name: gp.env.Metadata.Name,
Image: gp.env.Spec.Runtime.Image, Image: gp.env.Spec.Runtime.Image,
ImagePullPolicy: gp.runtimeImagePullPolicy, ImagePullPolicy: gp.runtimeImagePullPolicy,
@@ -560,7 +560,7 @@ func (gp *GenericPool) createPool() error {
}, },
}, },
}, },
}, }, gp.env.Spec.Runtime.Container),
{ {
Name: "fetcher", Name: "fetcher",
Image: gp.fetcherImage, Image: gp.fetcherImage,
Generated
+9 -9
View File
@@ -1,5 +1,5 @@
hash: 03ac7555eb1f5745e82abe42a07828ffcaac3785a77e7f37a596f30db8189e7f hash: 3b15246c5a7ca26271ef45e705c92351c786cfbf8ca1c093026b458e527ceefa
updated: 2018-02-08T22:40:25.586979477-08:00 updated: 2018-02-10T19:04:05.240303+01:00
imports: imports:
- name: cloud.google.com/go - name: cloud.google.com/go
version: 3b1ae45394a234c385be014e9a488f2bb6eef821 version: 3b1ae45394a234c385be014e9a488f2bb6eef821
@@ -18,7 +18,7 @@ imports:
- autorest/azure - autorest/azure
- autorest/date - autorest/date
- name: github.com/coreos/etcd - name: github.com/coreos/etcd
version: 6a265731e10a5137b991c1aa3a83ecefdd149d50 version: 9c6d93056575da4da94382f473b0ecfcd9c1443b
subpackages: subpackages:
- client - client
- name: github.com/davecgh/go-spew - name: github.com/davecgh/go-spew
@@ -55,7 +55,7 @@ imports:
- name: github.com/emicklei/go-restful-swagger12 - name: github.com/emicklei/go-restful-swagger12
version: dcef7f55730566d41eae5db10e7d6981829720f6 version: dcef7f55730566d41eae5db10e7d6981829720f6
- name: github.com/fsnotify/fsnotify - name: github.com/fsnotify/fsnotify
version: 4da3e2cfbabc9f751898f250b49f2439785783a1 version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9
- name: github.com/ghodss/yaml - name: github.com/ghodss/yaml
version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee version: 73d445a93680fa1a78ae23a5839bad48f32ba1ee
- name: github.com/go-openapi/analysis - name: github.com/go-openapi/analysis
@@ -104,7 +104,7 @@ imports:
- name: github.com/howeyc/gopass - name: github.com/howeyc/gopass
version: bf9dde6d0d2c004a008c27aaee91170c786f6db8 version: bf9dde6d0d2c004a008c27aaee91170c786f6db8
- name: github.com/imdario/mergo - name: github.com/imdario/mergo
version: 6633656539c1639d9d78127b7d47c622b5d7b6dc version: 163f41321a19dd09362d4c63cc2489db2015f1f4
- name: github.com/influxdata/influxdb - name: github.com/influxdata/influxdb
version: b7bb7e8359642b6e071735b50ae41f5eb343fd42 version: b7bb7e8359642b6e071735b50ae41f5eb343fd42
subpackages: subpackages:
@@ -133,7 +133,7 @@ imports:
subpackages: subpackages:
- pb - pb
- name: github.com/nats-io/nats-streaming-server - name: github.com/nats-io/nats-streaming-server
version: 6fdcdfbb2589e68692a68fd5b64b1b7e7c54bf05 version: 33414c6f2179201f7fda8743ba89d07184e3fa77
subpackages: subpackages:
- spb - spb
- util - util
@@ -148,7 +148,7 @@ imports:
subpackages: subpackages:
- xxHash32 - xxHash32
- name: github.com/pkg/errors - name: github.com/pkg/errors
version: f15c970de5b76fac0b59abb32d62c17cc7bed265 version: 30136e27e2ac8d167177e8a583aa4c3fea5be833
- name: github.com/PuerkitoBio/purell - name: github.com/PuerkitoBio/purell
version: 8a290539e2e8629dbc4e6bad948158f790ec31f4 version: 8a290539e2e8629dbc4e6bad948158f790ec31f4
- name: github.com/PuerkitoBio/urlesc - name: github.com/PuerkitoBio/urlesc
@@ -237,7 +237,7 @@ imports:
- name: k8s.io/api - name: k8s.io/api
version: 4b8fc5be9b77d91bbb6525d18591c43699a2b4e5 version: 4b8fc5be9b77d91bbb6525d18591c43699a2b4e5
- name: k8s.io/apiextensions-apiserver - name: k8s.io/apiextensions-apiserver
version: 0965a40c0530e110459750a7fcb9cfa4910fb944 version: fcd622fe88a4a6efcb5aea9e94ee87324ac1b036
subpackages: subpackages:
- pkg/apis/apiextensions - pkg/apis/apiextensions
- pkg/apis/apiextensions/v1beta1 - pkg/apis/apiextensions/v1beta1
@@ -245,7 +245,7 @@ imports:
- pkg/client/clientset/clientset/scheme - pkg/client/clientset/clientset/scheme
- pkg/client/clientset/clientset/typed/apiextensions/v1beta1 - pkg/client/clientset/clientset/typed/apiextensions/v1beta1
- name: k8s.io/apimachinery - name: k8s.io/apimachinery
version: 80184f5f100c67bfcda6b8a848e0243fe4bc2772 version: 8ab5f3d8a330c2e9baaf84e39042db8d49034ae2
subpackages: subpackages:
- pkg/api/equality - pkg/api/equality
- pkg/api/errors - pkg/api/errors
+2
View File
@@ -58,3 +58,5 @@ import:
version: ~1.2.1 version: ~1.2.1
- package: github.com/davecgh/go-spew - package: github.com/davecgh/go-spew
version: ~1.1.0 version: ~1.1.0
- package: github.com/imdario/mergo
version: ~0.3.2
+120
View File
@@ -0,0 +1,120 @@
/*
Copyright 2016 The Fission 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 fission
import (
"testing"
"github.com/stretchr/testify/assert"
apiv1 "k8s.io/client-go/pkg/api/v1"
)
func TestMergeContainerSpecs(t *testing.T) {
expected := apiv1.Container{
Name: "containerName",
Image: "testImage",
Command: []string{
"command",
},
Args: []string{
"arg1",
"arg2",
},
ImagePullPolicy: apiv1.PullNever,
TTY: true,
Env: []apiv1.EnvVar{
{
Name: "a",
Value: "b",
},
{
Name: "c",
Value: "d",
},
},
}
specs := []*apiv1.Container{
{
Name: "containerName",
Image: "testImage",
Command: []string{
"command",
},
Args: []string{
"arg1",
"arg2",
},
ImagePullPolicy: apiv1.PullNever,
TTY: true,
},
{
Name: "shouldNotBeThere",
Image: "shouldNotBeThere",
Env: []apiv1.EnvVar{
{
Name: "a",
Value: "b",
},
},
ImagePullPolicy: apiv1.PullAlways,
TTY: false,
},
{
Env: []apiv1.EnvVar{
{
Name: "c",
Value: "d",
},
},
ImagePullPolicy: apiv1.PullIfNotPresent,
TTY: false,
},
}
result := MergeContainerSpecs(specs...)
assert.Equal(t, expected, result)
// Check if merging order actually matters
var rspecs []*apiv1.Container
for i := len(specs) - 1; i >= 0; i -= 1 {
rspecs = append(rspecs, specs[i])
}
reverseResult := MergeContainerSpecs(rspecs...)
assert.NotEqual(t, expected, reverseResult)
}
func TestMergeContainerSpecsSingle(t *testing.T) {
expected := apiv1.Container{
Name: "containerName",
Image: "testImage",
Command: []string{
"command",
},
Args: []string{
"arg1",
"arg2",
},
ImagePullPolicy: apiv1.PullNever,
TTY: true,
}
result := MergeContainerSpecs(&expected)
assert.EqualValues(t, expected, result)
}
func TestMergeContainerSpecsNil(t *testing.T) {
expected := apiv1.Container{}
result := MergeContainerSpecs()
assert.EqualValues(t, expected, result)
}
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
. $(dirname $0)/test_utils.sh
FILE=$(pwd)/$1
FAILURES=0
run_test ${FILE}
exit $FAILURES
+19 -13
View File
@@ -389,28 +389,34 @@ run_all_tests() {
for file in $test_files for file in $test_files
do do
testname=${file#$ROOT/test/tests} run_test ${file}
testpath=$file done
}
if grep "^#test:disabled" $file run_test() {
file=$1
test_name=${file#${ROOT}/test/tests}
test_path=${file}
if grep "^#test:disabled" ${file}
then then
report_test_skipped $testname report_test_skipped ${test_name}
echo ------- Skipped $testname ------- echo ------- Skipped ${test_name} -------
else else
echo ------- Running $testname ------- echo ------- Running ${test_name} -------
pushd $(dirname $testpath) pushd $(dirname ${test_path})
if $testpath if ${test_path}
then then
echo SUCCESS: $testname echo [SUCCESS]: ${test_name}
report_test_passed $testname report_test_passed ${test_name}
else else
echo FAILED: $testname echo [FAILED]: ${test_name}
export FAILURES=$(($FAILURES+1)) export FAILURES=$(($FAILURES+1))
report_test_failed $testname report_test_failed ${test_name}
fi fi
popd popd
fi fi
done
} }
install_and_test() { install_and_test() {
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
set -euo pipefail
# test_env_vars.sh - tests whether a user is able to add environment variables to a Fission environment deployment
TEST_ID=$(date +%s)
ENV=python-${TEST_ID}
FN=foo-${TEST_ID}
RESOURCE_NS=default # Change to test-specific namespace once we support namespaced CRDs
FUNCTION_NS=${FUNCTION_NAMESPACE:-fission-function}
BUILDER_NS=fission-builder
# fs
TEST_DIR=/tmp/${TEST_ID}
ENV_SPEC_FILE=${TEST_DIR}/${ENV}.yaml
FN_FILE=${TEST_DIR}/${FN}.yaml
log_exec() {
cmd=$@
echo "> ${cmd}"
${cmd}
}
cleanup() {
log "Cleaning up..."
kubectl -n ${RESOURCE_NS} delete environment/${ENV} || true
rm -rf ${TEST_DIR}
}
cleanup
if [ -z "${TEST_NOCLEANUP:-}" ]; then
trap cleanup EXIT
else
log "TEST_NOCLEANUP is set; not cleaning up test artifacts afterwards."
fi
if ! stat ${TEST_DIR} >/dev/null 2>&1 ; then
mkdir ${TEST_DIR}
fi
getPodName() {
NS=$1
POD=$2
kubectl -n ${NS} get po -o jsonpath='{range .items[*]}{.metadata.name}{"\n"}{end}' \
| grep ${POD} \
| head -n 1
}
# retry function adapted from:
# https://unix.stackexchange.com/questions/82598/how-do-i-write-a-retry-logic-in-script-to-keep-retrying-to-run-it-upto-5-times/82610
function retry {
local n=1
local max=5
local delay=5
while true; do
"$@" && break || {
if [[ ${n} -lt ${max} ]]; then
((n++))
echo "Command '$@' failed. Attempt $n/$max:"
sleep ${delay};
else
>&2 echo "The command has failed after $n attempts."
exit 1;
fi
}
done
}
# Deploy environment (using kubectl because the Fission cli does not support the container arguments)
echo "Writing environment config to $ENV_SPEC_FILE"
cat > $ENV_SPEC_FILE <<- EOM
apiVersion: fission.io/v1
kind: Environment
metadata:
name: ${ENV}
namespace: ${RESOURCE_NS}
spec:
builder:
command: build
image: gcr.io/fission-ci/python-env-builder:test
container:
env:
- name: TEST_BUILDER_ENV_KEY
value: "TEST_BUILDER_ENV_VAR"
runtime:
image: gcr.io/fission-ci/python-env:test
container:
env:
- name: TEST_RUNTIME_ENV_KEY
value: "TEST_RUNTIME_ENV_VAR"
version: 2
poolsize: 1
EOM
log_exec kubectl -n ${RESOURCE_NS} apply -f ${ENV_SPEC_FILE}
sleep 15
# Wait for runtime and build env to be deployed
retry getPodName ${FUNCTION_NS} ${ENV} | grep '.\+'
runtimePod=$(getPodName ${FUNCTION_NS} ${ENV})
echo "function pod: ${runtimePod}."
retry getPodName ${BUILDER_NS} ${ENV} | grep '.\+'
buildPod=$(getPodName ${BUILDER_NS} ${ENV})
echo "builder pod: ${buildPod}."
# Ensure pods are running/ready
log "Waiting for ${FUNCTION_NS} ${ENV} to be available..."
echo "> kubectl -n ${FUNCTION_NS} exec ${runtimePod} -c ${ENV} env"
retry kubectl -n ${FUNCTION_NS} exec ${runtimePod} -c ${ENV} env > /dev/null
log "Runtime pod ready."
log "Waiting for ${BUILDER_NS} ${ENV} to be available..."
echo "> kubectl -n ${BUILDER_NS} exec ${buildPod} -c builder env"
retry kubectl -n ${BUILDER_NS} exec ${buildPod} -c builder env > /dev/null
log "Builder pod ready."
# Check if the env is set in the runtime
status=0
if kubectl -n ${FUNCTION_NS} exec ${runtimePod} -c ${ENV} env | grep TEST_RUNTIME_ENV_KEY=TEST_RUNTIME_ENV_VAR ; then
log "Runtime env is correct."
else
log "Runtime does not contain expected env var: TEST_RUNTIME_ENV_KEY=TEST_RUNTIME_ENV_VAR"
echo "--- Runtime Env ---"
kubectl -n ${FUNCTION_NS} exec ${runtimePod} -c ${ENV} env || true
echo "--- End Runtime Env ---"
status=5
fi
# Check if the env is set in the builder
if kubectl -n ${BUILDER_NS} exec ${buildPod} -c builder env | grep TEST_BUILDER_ENV_KEY=TEST_BUILDER_ENV_VAR ; then
log "Builder env is correct."
else
log "Builder does not contain expected env var: TEST_BUILDER_ENV_KEY=TEST_BUILDER_ENV_VAR"
echo "--- Builder Env ---"
kubectl -n ${BUILDER_NS} exec ${buildPod} -c builder env || true
echo "--- End Builder Env ---"
status=5
fi
exit ${status}
+25 -3
View File
@@ -18,7 +18,7 @@ package fission
import ( import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/pkg/api/v1" apiv1 "k8s.io/client-go/pkg/api/v1"
) )
type ( type (
@@ -133,7 +133,7 @@ type (
ConfigMaps []ConfigMapReference `json:"configmaps"` ConfigMaps []ConfigMapReference `json:"configmaps"`
// cpu and memory resources as per K8S standards // cpu and memory resources as per K8S standards
Resources v1.ResourceRequirements `json:"resources"` Resources apiv1.ResourceRequirements `json:"resources"`
// InvokeStrategy is a set of controls which affect how function executes // InvokeStrategy is a set of controls which affect how function executes
InvokeStrategy InvokeStrategy InvokeStrategy InvokeStrategy
@@ -208,6 +208,16 @@ type (
// server listens for function requests. Optional; // server listens for function requests. Optional;
// default 8888. // default 8888.
FunctionEndpointPort int32 `json:"functionendpointport"` FunctionEndpointPort int32 `json:"functionendpointport"`
// Container allows the modification of the deployed runtime
// container using the Kubernetes Container spec. Fission overrides
// the following fields:
// - Name
// - Image; set to the Runtime.Image
// - TerminationMessagePath
// - ImagePullPolicy
// (optional)
Container *apiv1.Container `json:"container,omitempty"`
} }
Builder struct { Builder struct {
// Image for containing the language runtime. // Image for containing the language runtime.
@@ -215,6 +225,18 @@ type (
// (Optional) Default build command to run for this build environment. // (Optional) Default build command to run for this build environment.
Command string `json:"command,omitempty"` Command string `json:"command,omitempty"`
// Container allows the modification of the deployed builder
// container using the Kubernetes Container spec. Fission overrides
// the following fields:
// - Name
// - Image; set to the Builder.Image
// - Command; set to the Builder.Command
// - TerminationMessagePath
// - ImagePullPolicy
// - ReadinessProbe
// (optional)
Container *apiv1.Container `json:"container,omitempty"`
} }
EnvironmentSpec struct { EnvironmentSpec struct {
// Environment API version // Environment API version
@@ -237,7 +259,7 @@ type (
AllowAccessToExternalNetwork bool `json:"allowAccessToExternalNetwork,omitempty"` AllowAccessToExternalNetwork bool `json:"allowAccessToExternalNetwork,omitempty"`
// Request and limit resources for the environment // Request and limit resources for the environment
Resources v1.ResourceRequirements `json:"resources"` Resources apiv1.ResourceRequirements `json:"resources"`
// The initial pool size for environment // The initial pool size for environment
Poolsize int `json:"poolsize,omitempty"` Poolsize int `json:"poolsize,omitempty"`