Skip to content

Commit 3c7e2fd

Browse files
tolushaclaude
andauthored
fix: convert non-string endpoint attributes to v1alpha1 (#1791)
Endpoint attributes are free-form (`map[string]apiext.JSON`) in v1alpha2 but string-based (`map[string]string`) in v1alpha1. Conversion from v1alpha2 is implemented as a JSON round-trip, so an endpoint holding a non-string attribute such as `discoverable: true` failed to convert with: cannot unmarshal bool into Go struct field Endpoint.container.endpoints.attributes of type string Rewrite the endpoint attributes of the marshalled component into their string representation before decoding into v1alpha1. Values keep their verbatim JSON text rather than going through `GetString`, so no precision is lost: `1048576` becomes "1048576" and not "1.048576e+06". The three conversion paths that decode a component from v1alpha2 are covered: plain components, plugin component overrides and parent component overrides, across the container, kubernetes and openshift component types. Signed-off-by: Anatolii Bazko <abazko@redhat.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent b772674 commit 3c7e2fd

5 files changed

Lines changed: 396 additions & 0 deletions

File tree

‎pkg/apis/workspaces/v1alpha1/component_plugin_conversion.go‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,10 @@ func convertPluginComponentSubComponentFrom_v1alpha2(src *v1alpha2.ComponentPlug
168168
if err != nil {
169169
return err
170170
}
171+
jsonComponent, err = stringifyComponentEndpointAttributes(jsonComponent)
172+
if err != nil {
173+
return err
174+
}
171175
err = json.Unmarshal(jsonComponent, &dest)
172176
if err != nil {
173177
return err

‎pkg/apis/workspaces/v1alpha1/components_conversion.go‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@ func convertComponentFrom_v1alpha2(src *v1alpha2.Component, dest *Component) err
5656
if err != nil {
5757
return err
5858
}
59+
jsonComponent, err = stringifyComponentEndpointAttributes(jsonComponent)
60+
if err != nil {
61+
return err
62+
}
5963
err = json.Unmarshal(jsonComponent, dest)
64+
if err != nil {
65+
return err
66+
}
6067
switch {
6168
case dest.Container != nil:
6269
dest.Container.Name = name
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
//
2+
//
3+
// Copyright Red Hat
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package v1alpha1
18+
19+
import (
20+
"encoding/json"
21+
22+
"github.com/devfile/api/v2/pkg/attributes"
23+
)
24+
25+
// componentsWithEndpoints lists the keys of the component types that declare endpoints.
26+
var componentsWithEndpoints = []string{"container", "kubernetes", "openshift"}
27+
28+
// stringifyComponentEndpointAttributes rewrites the endpoint attributes of a marshalled v1alpha2
29+
// component so that it can be decoded into its v1alpha1 counterpart.
30+
//
31+
// Endpoint attributes are free-form (`map[string]apiext.JSON`) in v1alpha2 but string-based
32+
// (`map[string]string`) in v1alpha1, so an attribute such as `discoverable: true` is valid in
33+
// v1alpha2 yet cannot be decoded as-is here. Conversion from v1alpha2 is implemented as a JSON
34+
// round-trip, so without this the conversion of a component holding an endpoint with a non-string
35+
// attribute would fail with "cannot unmarshal bool into Go struct field
36+
// Endpoint.container.endpoints.attributes of type string".
37+
//
38+
// All the v1alpha2 component flavours (`Component`, `ComponentPluginOverride` and
39+
// `ComponentParentOverride`) share the same JSON representation, so this operates on the
40+
// marshalled bytes rather than on the Go types.
41+
func stringifyComponentEndpointAttributes(data []byte) ([]byte, error) {
42+
component := map[string]json.RawMessage{}
43+
if err := json.Unmarshal(data, &component); err != nil {
44+
return nil, err
45+
}
46+
47+
rewritten := false
48+
for _, componentType := range componentsWithEndpoints {
49+
body, found := component[componentType]
50+
if !found {
51+
continue
52+
}
53+
updatedBody, updated, err := stringifyEndpointAttributes(body)
54+
if err != nil {
55+
return nil, err
56+
}
57+
if updated {
58+
component[componentType] = updatedBody
59+
rewritten = true
60+
}
61+
}
62+
63+
// Leave the document untouched when there is nothing to convert.
64+
if !rewritten {
65+
return data, nil
66+
}
67+
return json.Marshal(component)
68+
}
69+
70+
// stringifyEndpointAttributes rewrites the attributes of the endpoints declared by a marshalled
71+
// component body, and reports whether anything was rewritten.
72+
func stringifyEndpointAttributes(body json.RawMessage) (json.RawMessage, bool, error) {
73+
componentBody := map[string]json.RawMessage{}
74+
if err := json.Unmarshal(body, &componentBody); err != nil {
75+
return nil, false, err
76+
}
77+
rawEndpoints, found := componentBody["endpoints"]
78+
if !found {
79+
return nil, false, nil
80+
}
81+
82+
var endpoints []map[string]json.RawMessage
83+
if err := json.Unmarshal(rawEndpoints, &endpoints); err != nil {
84+
return nil, false, err
85+
}
86+
87+
rewritten := false
88+
for _, endpoint := range endpoints {
89+
rawAttributes, found := endpoint["attributes"]
90+
if !found {
91+
continue
92+
}
93+
freeFormAttributes := attributes.Attributes{}
94+
if err := json.Unmarshal(rawAttributes, &freeFormAttributes); err != nil {
95+
return nil, false, err
96+
}
97+
if len(freeFormAttributes) == 0 {
98+
continue
99+
}
100+
stringAttributes, err := json.Marshal(stringifyAttributes(freeFormAttributes))
101+
if err != nil {
102+
return nil, false, err
103+
}
104+
endpoint["attributes"] = stringAttributes
105+
rewritten = true
106+
}
107+
108+
if !rewritten {
109+
return nil, false, nil
110+
}
111+
112+
updatedEndpoints, err := json.Marshal(endpoints)
113+
if err != nil {
114+
return nil, false, err
115+
}
116+
componentBody["endpoints"] = updatedEndpoints
117+
118+
updatedBody, err := json.Marshal(componentBody)
119+
if err != nil {
120+
return nil, false, err
121+
}
122+
return updatedBody, true, nil
123+
}
124+
125+
// stringifyAttributes converts free-form attributes into the string-based map v1alpha1 expects.
126+
// A JSON string is unquoted; every other value (boolean, number, object, array) keeps its verbatim
127+
// JSON text, so that no precision is lost: `1048576` becomes "1048576", and not the "1.048576e+06"
128+
// that `attributes.Attributes.GetString` would produce.
129+
//
130+
// The conversion is one way. Nothing parses these strings back when converting to v1alpha2 again,
131+
// so `discoverable: true` comes out of a v1alpha2 -> v1alpha1 -> v1alpha2 round-trip as the string
132+
// "true". Scalars stay usable, because `GetBoolean` and `GetNumber` fall back to strconv when the
133+
// attribute holds a string, but an object or an array comes back as a string and no longer decodes
134+
// with `GetInto`. Parsing the strings back is not an option: a string attribute the user actually
135+
// authored as "true" cannot be told apart from a stringified boolean.
136+
func stringifyAttributes(attrs attributes.Attributes) map[string]string {
137+
stringAttributes := make(map[string]string, len(attrs))
138+
for key, value := range attrs {
139+
// A JSON `null` is decoded into an empty Raw by apiext.JSON.
140+
if len(value.Raw) == 0 {
141+
stringAttributes[key] = "null"
142+
continue
143+
}
144+
var stringValue string
145+
if err := json.Unmarshal(value.Raw, &stringValue); err != nil {
146+
stringValue = string(value.Raw)
147+
}
148+
stringAttributes[key] = stringValue
149+
}
150+
return stringAttributes
151+
}

0 commit comments

Comments
 (0)