Skip to content

Commit 9aa3fb4

Browse files
authored
Merge pull request #121 from A-Hilaly/multiversion-package
Bring in `pkg/model/multiversion` package to help interacting with multiple API versions
2 parents 1423dc8 + 58764f2 commit 9aa3fb4

File tree

16 files changed

+4774
-18
lines changed

16 files changed

+4774
-18
lines changed

pkg/model/crd.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,36 @@ func (r *CRD) ListOpMatchFieldNames() []string {
618618
return r.cfg.ListOpMatchFieldNames(r.Names.Original)
619619
}
620620

621+
// GetAllRenames returns all the field renames observed in the generator config
622+
// for a given OpType.
623+
func (r *CRD) GetAllRenames(op OpType) (map[string]string, error) {
624+
renames := make(map[string]string)
625+
resourceConfig, ok := r.cfg.Resources[r.Names.Original]
626+
if !ok {
627+
return renames, nil
628+
}
629+
630+
opMap := r.sdkAPI.GetOperationMap(r.cfg)
631+
operations := (*opMap)[op]
632+
633+
if resourceConfig.Renames == nil || resourceConfig.Renames.Operations == nil {
634+
return renames, nil
635+
}
636+
637+
opRenameConfigs := resourceConfig.Renames.Operations
638+
for opName, opRenameConfigs := range opRenameConfigs {
639+
for _, op := range operations {
640+
if opName != op.Name {
641+
continue
642+
}
643+
for old, new := range opRenameConfigs.InputFields {
644+
renames[old] = new
645+
}
646+
}
647+
}
648+
return renames, nil
649+
}
650+
621651
// NewCRD returns a pointer to a new `ackmodel.CRD` struct that describes a
622652
// single top-level resource in an AWS service API
623653
func NewCRD(

pkg/model/multiversion/api_info.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package multiversion
15+
16+
// TODO(a-hilaly) move this file outside of pkg/model/multiversion. Idealy we
17+
// Should be able to access APIStatus and APIInfo to prevent regenerating removed or
18+
// deprecated APIs.
19+
20+
type APIStatus string
21+
22+
const (
23+
APIStatusUnknown APIStatus = "unknown"
24+
APIStatusAvailable = "available"
25+
APIStatusRemoved = "removed"
26+
APIStatusDeprecated = "deprecated"
27+
)
28+
29+
// APIInfo contains information related a specific apiVersion.
30+
type APIInfo struct {
31+
// The API status. Can be one of Available, Removed and Deprecated.
32+
Status APIStatus
33+
// the aws-sdk-go version used to generated the apiVersion.
34+
AWSSDKVersion string
35+
// Full path of the generator config file.
36+
GeneratorConfigPath string
37+
// The API version.
38+
APIVersion string
39+
}

pkg/model/multiversion/delta.go

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"). You may
4+
// not use this file except in compliance with the License. A copy of the
5+
// License is located at
6+
//
7+
// http://aws.amazon.com/apache2.0/
8+
//
9+
// or in the "license" file accompanying this file. This file is distributed
10+
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
// express or implied. See the License for the specific language governing
12+
// permissions and limitations under the License.
13+
14+
package multiversion
15+
16+
import (
17+
"fmt"
18+
"sort"
19+
20+
awssdkmodel "github.com/aws/aws-sdk-go/private/model/api"
21+
22+
ackmodel "github.com/aws-controllers-k8s/code-generator/pkg/model"
23+
)
24+
25+
// FieldChangeType represents the type of field modification.
26+
//
27+
// - FieldChangeTypeUnknown is used when ChangeType cannot be computed.
28+
// - FieldChangeTypeNone is used when a field name and structure didn't change.
29+
// - FieldChangeTypeAdded is used when a new field is introduced in a CRD.
30+
// - FieldChangeTypeRemoved is used a when a field is removed from a CRD.
31+
// - FieldChangeTypeRenamed is used when a field is renamed.
32+
// - FieldChangeTypeShapeChanged is used when a field shape has changed.
33+
// - FieldChangeTypeShapeChangedFromStringToSecret is used when a field change to
34+
// a k8s secret type.
35+
// - FieldChangeTypeShapeChangedFromSecretToString is used when a field changed from
36+
// a k8s secret to a Go string.
37+
type FieldChangeType string
38+
39+
const (
40+
FieldChangeTypeUnknown FieldChangeType = "unknown"
41+
FieldChangeTypeNone FieldChangeType = "none"
42+
FieldChangeTypeAdded FieldChangeType = "added"
43+
FieldChangeTypeRemoved FieldChangeType = "removed"
44+
FieldChangeTypeRenamed FieldChangeType = "renamed"
45+
FieldChangeTypeShapeChanged FieldChangeType = "shape-changed"
46+
FieldChangeTypeShapeChangedFromStringToSecret FieldChangeType = "shape-changed-from-string-to-secret"
47+
FieldChangeTypeShapeChangedFromSecretToString FieldChangeType = "shape-changed-from-secret-to-string"
48+
)
49+
50+
// FieldDelta represents the delta between the same original field in two
51+
// different CRD versions. If a field is removed in the Destination version
52+
// the Destination value will be nil. If a field is new in the Destination
53+
// version, the Source value will be nil.
54+
type FieldDelta struct {
55+
ChangeType FieldChangeType
56+
// Field from the destination CRD
57+
Destination *ackmodel.Field
58+
// Field from the source CRD
59+
Source *ackmodel.Field
60+
}
61+
62+
// CRDDelta stores the spec and status deltas for a custom resource.
63+
type CRDDelta struct {
64+
SpecDeltas []FieldDelta
65+
StatusDeltas []FieldDelta
66+
}
67+
68+
// ComputeCRDFieldDeltas compares two ackmodel.CRD instances and returns the
69+
// spec and status fields deltas. src is the CRD of the spoke (source) version
70+
// and dst is the CRD of the hub (destination) version.
71+
func ComputeCRDFieldDeltas(src, dst *ackmodel.CRD) (*CRDDelta, error) {
72+
dstRenames, err := dst.GetAllRenames(ackmodel.OpTypeCreate)
73+
if err != nil {
74+
return nil, fmt.Errorf("cannot get resource field renames: %s", err)
75+
}
76+
77+
srcRenames, err := src.GetAllRenames(ackmodel.OpTypeCreate)
78+
if err != nil {
79+
return nil, fmt.Errorf("cannot get resource field renames: %s", err)
80+
}
81+
82+
renames, err := ComputeRenamesDelta(srcRenames, dstRenames)
83+
if err != nil {
84+
return nil, fmt.Errorf("cannot compute the field renames delta: %v", err)
85+
}
86+
87+
specDeltas, err := ComputeFieldDeltas(src.SpecFields, dst.SpecFields, renames)
88+
if err != nil {
89+
return nil, fmt.Errorf("cannot compute spec fields deltas: %s", err)
90+
}
91+
92+
statusDeltas, err := ComputeFieldDeltas(src.StatusFields, dst.StatusFields, renames)
93+
if err != nil {
94+
return nil, fmt.Errorf("cannot compute status fields deltas: %s", err)
95+
}
96+
97+
return &CRDDelta{
98+
specDeltas,
99+
statusDeltas,
100+
}, nil
101+
}
102+
103+
// fieldChangedToSecret returns true if field changed from string to secret.
104+
func fieldChangedToSecret(src, dst *ackmodel.Field) bool {
105+
return (src.FieldConfig == nil ||
106+
(src.FieldConfig != nil && !src.FieldConfig.IsSecret)) &&
107+
(dst.FieldConfig != nil && dst.FieldConfig.IsSecret)
108+
}
109+
110+
// ComputeFieldDeltas computes the difference between two maps of fields. It returns a list
111+
// of FieldDelta's that contains the ChangeType and at least one field reference.
112+
func ComputeFieldDeltas(
113+
srcFields map[string]*ackmodel.Field,
114+
dstFields map[string]*ackmodel.Field,
115+
// the renames delta renames
116+
renames map[string]string,
117+
) ([]FieldDelta, error) {
118+
deltas := []FieldDelta{}
119+
120+
// collect field names and sort them to ensure a determenistic output order.
121+
srcNames := []string{}
122+
for name := range srcFields {
123+
srcNames = append(srcNames, name)
124+
}
125+
sort.Strings(srcNames)
126+
127+
dstNames := []string{}
128+
for name := range dstFields {
129+
dstNames = append(dstNames, name)
130+
}
131+
sort.Strings(dstNames)
132+
133+
// let's make sure we don't visit fields more than once - especially
134+
// when fields are renamed.
135+
visitedFields := map[string]bool{}
136+
137+
// first let's loop over the srcNames array and see if we can find
138+
// the same field name in dstNames.
139+
for _, srcName := range srcNames {
140+
srcField, _ := srcFields[srcName]
141+
dstField, ok := dstFields[srcName]
142+
// If a field is found in both arrays only three changes are possible:
143+
// None, TypeChange and ChangeTypeShapeChangedToSecret.
144+
// NOTE(a-hilaly): carefull about X -> Y then Z -> X renames. It should
145+
// not be allowed.
146+
if ok {
147+
// mark field as visited.
148+
visitedFields[srcName] = true
149+
// check if field change from string to secret
150+
if fieldChangedToSecret(srcField, dstField) {
151+
deltas = append(deltas, FieldDelta{
152+
Source: srcField,
153+
Destination: dstField,
154+
ChangeType: FieldChangeTypeShapeChangedFromStringToSecret,
155+
})
156+
continue
157+
}
158+
// check if field changed from secret to string
159+
if fieldChangedToSecret(dstField, srcField) {
160+
deltas = append(deltas, FieldDelta{
161+
Source: srcField,
162+
Destination: dstField,
163+
ChangeType: FieldChangeTypeShapeChangedFromSecretToString,
164+
})
165+
continue
166+
}
167+
168+
equalShapes, _ := AreEqualShapes(srcField.ShapeRef.Shape, dstField.ShapeRef.Shape, true)
169+
if equalShapes {
170+
// if the fields have equal names and types the change is intact
171+
deltas = append(deltas, FieldDelta{
172+
Source: srcField,
173+
Destination: dstField,
174+
ChangeType: FieldChangeTypeNone,
175+
})
176+
continue
177+
}
178+
179+
// at this point we know that the fields kept the same name but have different
180+
// shapes
181+
deltas = append(deltas, FieldDelta{
182+
Source: srcField,
183+
Destination: dstField,
184+
ChangeType: FieldChangeTypeShapeChanged,
185+
})
186+
continue
187+
}
188+
189+
// if a field is not found in the dstNames, there are three
190+
// possible changes: Removed, Added or Renamed.
191+
192+
// First let's check if field was renamed
193+
newName, ok := renames[srcName]
194+
if ok {
195+
dstField, ok2 := dstFields[newName]
196+
if !ok2 {
197+
// if a field was renamed and we can't find it in dstNames, something
198+
// very wrong happend during CRD loading.
199+
return nil, fmt.Errorf("cannot find renamed field %s " + newName)
200+
}
201+
202+
// mark field as visited, both old and new names.
203+
visitedFields[newName] = true
204+
visitedFields[srcName] = true
205+
206+
// this will mostlikely be always true, but let's double check.
207+
if newName == dstField.Names.Camel {
208+
// field was renamed
209+
deltas = append(deltas, FieldDelta{
210+
Source: srcField,
211+
Destination: dstField,
212+
ChangeType: FieldChangeTypeRenamed,
213+
})
214+
continue
215+
}
216+
return nil, fmt.Errorf("renamed field unmatching: %v != %v", newName, dstField.Names.Camel)
217+
}
218+
219+
// If the field was not renamed nor left intact nor it shape changed, it's
220+
// a removed field.
221+
deltas = append(deltas, FieldDelta{
222+
Source: srcField,
223+
Destination: nil,
224+
ChangeType: FieldChangeTypeRemoved,
225+
})
226+
}
227+
228+
// At this point we collected every type of changes except added fields.
229+
// To find added fields we just look for fields that are in dstNames and
230+
// were not visited before (are not in srcNames).
231+
for _, dstName := range dstNames {
232+
if _, visited := visitedFields[dstName]; visited {
233+
continue
234+
}
235+
dstField, _ := dstFields[dstName]
236+
deltas = append(deltas, FieldDelta{
237+
Source: nil,
238+
Destination: dstField,
239+
ChangeType: FieldChangeTypeAdded,
240+
})
241+
}
242+
243+
sort.Slice(deltas, func(i, j int) bool {
244+
return getFieldNameFromDelta(deltas[i]) < getFieldNameFromDelta(deltas[j])
245+
})
246+
return deltas, nil
247+
}
248+
249+
// getFieldNameFromDelta retrieves the field name from a FieldDelta. If a field is
250+
// renamed it will return the new name.
251+
func getFieldNameFromDelta(delta FieldDelta) string {
252+
if delta.ChangeType == FieldChangeTypeRemoved {
253+
return delta.Source.Names.Camel
254+
}
255+
return delta.Destination.Names.Camel
256+
}
257+
258+
// AreEqualShapes returns whether two awssdkmodel.ShapeRef are equal or not. When the two
259+
// given shapes are not equal, it will return an error representing the first type mismatch
260+
// detected.
261+
func AreEqualShapes(a, b *awssdkmodel.Shape, allowMemberNamesInequality bool) (bool, error) {
262+
if a.Type != b.Type {
263+
return false, fmt.Errorf("found different shape types (%s and %s)", a.ShapeName, a.ShapeName)
264+
}
265+
if !allowMemberNamesInequality && a.ShapeName != b.ShapeName {
266+
return false, fmt.Errorf("found different shape names (%s and %s)", a.ShapeName, a.ShapeName)
267+
}
268+
269+
switch a.Type {
270+
case "structure":
271+
// verify that both structs have the same member names
272+
if len(a.MemberNames()) != len(a.MemberNames()) {
273+
return false, fmt.Errorf("found different MemberNames size in %s", a.ShapeName)
274+
}
275+
276+
// loop over the struct members and verify that if they have the same
277+
// name they should have the same shape.
278+
for _, memberName := range a.MemberNames() {
279+
memberRefA := a.MemberRefs[memberName]
280+
memberRefB, ok := b.MemberRefs[memberName]
281+
if !ok {
282+
return false, fmt.Errorf("missing member %s in %s", memberName, memberRefA.ShapeName)
283+
}
284+
// if two members with the same name doesn't have the same shape
285+
// return false.
286+
if equal, err := AreEqualShapes(memberRefA.Shape, memberRefB.Shape, false); !equal {
287+
return false, fmt.Errorf("member %s have two different shapes in %s: %v", memberName, memberRefA.ShapeName, err)
288+
}
289+
}
290+
case "map":
291+
// for maps we check that the keys and values have the same types
292+
if equal, err := AreEqualShapes(a.KeyRef.Shape, b.KeyRef.Shape, false); !equal {
293+
return false, fmt.Errorf("map key shape mismatch in %s: %v", a.ShapeName, err)
294+
}
295+
if equal, err := AreEqualShapes(a.ValueRef.Shape, b.ValueRef.Shape, false); !equal {
296+
return false, fmt.Errorf("map value shape mismatch in %s: %v", a.ShapeName, err)
297+
}
298+
case "list":
299+
// for lists we check that the members have the same types
300+
if equal, err := AreEqualShapes(a.MemberRef.Shape, b.MemberRef.Shape, false); !equal {
301+
return false, fmt.Errorf("member shape mismatch in %s: %v", a.ShapeName, err)
302+
}
303+
}
304+
305+
return true, nil
306+
}

0 commit comments

Comments
 (0)