Skip to content

Commit b20ae0f

Browse files
committed
allow namespace specification via parameter in templates
1 parent c07de24 commit b20ae0f

File tree

6 files changed

+163
-9
lines changed

6 files changed

+163
-9
lines changed

pkg/config/cmd/cmd.go

+3
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ func HaltOnError(fn AfterFunc) AfterFunc {
131131

132132
// Create is the default create operation for a generic resource.
133133
func Create(info *resource.Info, namespace string, obj runtime.Object) (runtime.Object, error) {
134+
if len(info.Namespace) > 0 {
135+
namespace = info.Namespace
136+
}
134137
return resource.NewHelper(info.Client, info.Mapping).Create(namespace, false, obj)
135138
}
136139

pkg/template/api/types.go

+5
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type Template struct {
2626
Parameters []Parameter
2727

2828
// objects is an array of resources to include in this template.
29+
// If a namespace value is hardcoded in the object, it will be removed
30+
// during template instantiation, however if the namespace value
31+
// is, or container, a ${PARAMETER_REFERENCE}, the resolved
32+
// value after parameter substitution will be respected and the object
33+
// will be created in that namespace.
2934
Objects []runtime.Object
3035

3136
// objectLabels is an optional set of labels that are applied to every

pkg/template/api/v1/types.go

+5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ type Template struct {
2323
Message string `json:"message,omitempty" protobuf:"bytes,2,opt,name=message"`
2424

2525
// objects is an array of resources to include in this template.
26+
// If a namespace value is hardcoded in the object, it will be removed
27+
// during template instantiation, however if the namespace value
28+
// is, or container, a ${PARAMETER_REFERENCE}, the resolved
29+
// value after parameter substitution will be respected and the object
30+
// will be created in that namespace.
2631
Objects []runtime.RawExtension `json:"objects" protobuf:"bytes,3,rep,name=objects"`
2732

2833
// parameters is an optional array of Parameters used during the

pkg/template/template.go

+10-9
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,16 @@ func (p *Processor) Process(template *api.Template) field.ErrorList {
6565
item = decodedObj
6666
}
6767

68+
// If an object definition's metadata includes a hardcoded namespace field, the field will be stripped out of
69+
// the definition during template instantiation. Namespace fields that contain a ${PARAMETER_REFERENCE}
70+
// will be left in place, resolved during parameter substition, and the object will be created in the
71+
// referenced namespace.
72+
stripNamespace(item)
73+
6874
newItem, err := p.SubstituteParameters(paramMap, item)
6975
if err != nil {
7076
templateErrors = append(templateErrors, field.Invalid(idxPath.Child("parameters"), template.Parameters, err.Error()))
7177
}
72-
// If an object definition's metadata includes a namespace field, the field will be stripped out of
73-
// the definition during template instantiation. This is necessary because all objects created during
74-
// instantiation are placed into the target namespace, so it would be invalid for the object to declare
75-
//a different namespace.
76-
stripNamespace(newItem)
7778
if err := util.AddObjectLabels(newItem, template.ObjectLabels); err != nil {
7879
templateErrors = append(templateErrors, field.Invalid(idxPath.Child("labels"),
7980
template.ObjectLabels, fmt.Sprintf("label could not be applied: %v", err)))
@@ -85,22 +86,22 @@ func (p *Processor) Process(template *api.Template) field.ErrorList {
8586
}
8687

8788
func stripNamespace(obj runtime.Object) {
88-
// Remove namespace from the item
89-
if itemMeta, err := meta.Accessor(obj); err == nil && len(itemMeta.GetNamespace()) > 0 {
89+
// Remove namespace from the item unless it contains a ${PARAMETER_REFERENCE}
90+
if itemMeta, err := meta.Accessor(obj); err == nil && len(itemMeta.GetNamespace()) > 0 && !stringParameterExp.MatchString(itemMeta.GetNamespace()) {
9091
itemMeta.SetNamespace("")
9192
return
9293
}
9394
// TODO: allow meta.Accessor to handle runtime.Unstructured
9495
if unstruct, ok := obj.(*runtime.Unstructured); ok && unstruct.Object != nil {
9596
if obj, ok := unstruct.Object["metadata"]; ok {
9697
if m, ok := obj.(map[string]interface{}); ok {
97-
if _, ok := m["namespace"]; ok {
98+
if _, ok := m["namespace"]; ok && !stringParameterExp.MatchString(m["namespace"].(string)) {
9899
m["namespace"] = ""
99100
}
100101
}
101102
return
102103
}
103-
if _, ok := unstruct.Object["namespace"]; ok {
104+
if _, ok := unstruct.Object["namespace"]; ok && stringParameterExp.MatchString(unstruct.Object["namespace"].(string)) {
104105
unstruct.Object["namespace"] = ""
105106
return
106107
}

test/cmd/newapp.sh

+19
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ trap os::test::junit::reconcile_output EXIT
66
(
77
set +e
88
# oc delete all,templates --all
9+
oc delete-project template-substitute
10+
oc delete-project prefix-template-substitute
911
exit 0
1012
) &>/dev/null
1113

@@ -50,6 +52,23 @@ os::cmd::expect_failure 'oc get dc/mysql'
5052
os::cmd::expect_failure 'oc get dc/php'
5153
os::cmd::expect_success_and_text 'oc new-app -f test/testdata/template-without-app-label.json -o yaml' 'app: ruby-helloworld-sample'
5254

55+
# check object namespace handling
56+
# hardcoded values should be stripped
57+
os::cmd::expect_success_and_not_text 'oc new-app -f test/testdata/template-with-namespaces.json -o jsonpath="{.items[?(@.metadata.name==\"stripped\")].metadata.namespace}"' 'STRIPPED'
58+
# normal parameterized values should be substituted and retained
59+
os::cmd::expect_success_and_text 'oc new-app -f test/testdata/template-with-namespaces.json -o jsonpath="{.items[?(@.metadata.name==\"route-edge-substituted\")].metadata.namespace}"' 'substituted'
60+
os::cmd::expect_success_and_text 'oc new-app -f test/testdata/template-with-namespaces.json -o jsonpath="{.items[?(@.metadata.name==\"route-edge-prefix-substituted\")].metadata.namespace}"' 'prefix-substituted'
61+
# non-string parameterized values should be stripped
62+
os::cmd::expect_failure_and_text 'oc new-app -f test/testdata/template-with-namespaces.json -o jsonpath="{.items[?(@.metadata.name==\"route-edge-refstripped\")].metadata.namespace}"' 'namespace is not found'
63+
os::cmd::expect_failure_and_text 'oc new-app -f test/testdata/template-with-namespaces.json -o jsonpath="{.items[?(@.metadata.name==\"route-edge-prefix-refstripped\")].metadata.namespace}"' 'namespace is not found'
64+
# ensure the objects can actually get created with a namespace specified
65+
project=$(oc project -q)
66+
os::cmd::expect_success 'oc new-project template-substitute'
67+
os::cmd::expect_success 'oc new-project prefix-template-substitute'
68+
os::cmd::expect_success 'oc project ${project}'
69+
os::cmd::expect_success 'oc new-app -f test/testdata/template-with-namespaces.json -p SUBSTITUTED=template-substitute'
70+
os::cmd::expect_success 'oc delete all -l app=ruby-helloworld-sample'
71+
5372
# ensure non-duplicate invalid label errors show up
5473
os::cmd::expect_failure_and_text 'oc new-app nginx -l qwer1345%$$#=self' 'error: ImageStream "nginx" is invalid'
5574
os::cmd::expect_failure_and_text 'oc new-app nginx -l qwer1345%$$#=self' 'DeploymentConfig "nginx" is invalid'
+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
{
2+
"kind": "Template",
3+
"apiVersion": "v1",
4+
"metadata": {
5+
"name": "ruby-helloworld-sample",
6+
"creationTimestamp": null,
7+
"annotations": {
8+
"description": "some objects in this template declare their own namespace via a parameter confirm new-app will tolerate it",
9+
"iconClass": "icon-ruby",
10+
"tags": "instant-app,ruby,mysql"
11+
}
12+
},
13+
"objects": [
14+
{
15+
"kind": "Route",
16+
"apiVersion": "v1",
17+
"metadata": {
18+
"name": "route-edge-stripped",
19+
"namespace": "STRIPPED"
20+
},
21+
"spec": {
22+
"host": "www.example.com",
23+
"to": {
24+
"kind": "Service",
25+
"name": "frontend"
26+
},
27+
"tls": {
28+
"termination": "edge"
29+
}
30+
},
31+
"status": {}
32+
},
33+
{
34+
"kind": "Route",
35+
"apiVersion": "v1",
36+
"metadata": {
37+
"name": "route-edge-substituted",
38+
"namespace": "${SUBSTITUTED}"
39+
},
40+
"spec": {
41+
"host": "www.example.com",
42+
"to": {
43+
"kind": "Service",
44+
"name": "frontend"
45+
},
46+
"tls": {
47+
"termination": "edge"
48+
}
49+
},
50+
"status": {}
51+
},
52+
{
53+
"kind": "Route",
54+
"apiVersion": "v1",
55+
"metadata": {
56+
"name": "route-edge-prefix-substituted",
57+
"namespace": "prefix-${SUBSTITUTED}"
58+
},
59+
"spec": {
60+
"host": "www.example.com",
61+
"to": {
62+
"kind": "Service",
63+
"name": "frontend"
64+
},
65+
"tls": {
66+
"termination": "edge"
67+
}
68+
},
69+
"status": {}
70+
},
71+
{
72+
"kind": "Route",
73+
"apiVersion": "v1",
74+
"metadata": {
75+
"name": "route-edge-refstripped",
76+
"namespace": "${{SUBSTITUTED}}"
77+
},
78+
"spec": {
79+
"host": "www.example.com",
80+
"to": {
81+
"kind": "Service",
82+
"name": "frontend"
83+
},
84+
"tls": {
85+
"termination": "edge"
86+
}
87+
},
88+
"status": {}
89+
},
90+
{
91+
"kind": "Route",
92+
"apiVersion": "v1",
93+
"metadata": {
94+
"name": "route-edge-prefix-refstripped",
95+
"namespace": "prefix-${{SUBSTITUTED}}"
96+
},
97+
"spec": {
98+
"host": "www.example.com",
99+
"to": {
100+
"kind": "Service",
101+
"name": "frontend"
102+
},
103+
"tls": {
104+
"termination": "edge"
105+
}
106+
},
107+
"status": {}
108+
}
109+
],
110+
"parameters": [
111+
{
112+
"name": "SUBSTITUTED",
113+
"description": "namespace value",
114+
"value": "substituted",
115+
"required": true
116+
}
117+
],
118+
"labels": {
119+
"template": "application-template-stibuild"
120+
}
121+
}

0 commit comments

Comments
 (0)