package cmd import ( "fmt" "reflect" "strings" "sigs.k8s.io/yaml" ) type ( manifest = map[string]interface{} diff struct { path []string a interface{} b interface{} } ) func splitManifests(manifest string) []string { manifests := strings.Split(manifest, "\n---\n") filtered := []string{} for _, m := range manifests { if !isManifestEmpty(m) { filtered = append(filtered, m) } } return filtered } func isManifestEmpty(manifest string) bool { lines := strings.Split(manifest, "\n") for _, line := range lines { if line == "" || strings.HasPrefix(line, "#") || line == "---" { continue } return false } return true } func manifestKey(m manifest) string { kind := m["kind"].(string) meta := m["metadata"].(map[string]interface{}) name := meta["name"].(string) return fmt.Sprintf("%s/%s", kind, name) } func (d diff) String() string { expected, _ := yaml.Marshal(d.a) actual, _ := yaml.Marshal(d.b) return fmt.Sprintf("Diff at [%s]:\nExpected:\n%s\nActual:\n%s", d.path, string(expected), string(actual)) } func parseManifestList(in string) map[string]manifest { manifestList := splitManifests(in) manifestMap := map[string]manifest{} for _, m := range manifestList { manifest := manifest{} yaml.Unmarshal([]byte(m), &manifest) manifestMap[manifestKey(manifest)] = manifest } return manifestMap } func diffManifest(a manifest, b manifest, path []string) []diff { diffs := []diff{} for k, v := range a { bv, bvExists := b[k] switch val := v.(type) { case manifest: if !bvExists { diffs = append(diffs, diff{ path: extend(path, k), a: val, b: nil, }) } else { bvm, ok := bv.(manifest) if !ok { diffs = append(diffs, diff{ path: extend(path, k), a: val, b: bv, }) } else { diffs = append(diffs, diffManifest(val, bvm, extend(path, k))...) } } case []interface{}: bva, ok := bv.([]interface{}) if !ok { diffs = append(diffs, diff{ path: extend(path, k), a: val, b: bv, }) } else if len(val) != len(bva) { diffs = append(diffs, diff{ path: extend(path, k), a: val, b: bva, }) } else { diffs = append(diffs, diffArray(val, bva, extend(path, k))...) } default: if !bvExists { diffs = append(diffs, diff{ path: extend(path, k), a: val, b: nil, }) } else if !reflect.DeepEqual(val, bv) { diffs = append(diffs, diff{ path: extend(path, k), a: val, b: bv, }) } } } for k, v := range b { _, avExists := a[k] if !avExists { diffs = append(diffs, diff{ path: extend(path, k), a: nil, b: v, }) } } return diffs } func diffArray(a, b []interface{}, path []string) []diff { diffs := []diff{} for i, v := range a { switch aVal := v.(type) { case manifest: bm, ok := b[i].(manifest) if !ok { diffs = append(diffs, diff{ path: extend(path, fmt.Sprintf("%d", i)), a: aVal, b: b[i], }) } else { diffs = append(diffs, diffManifest(aVal, bm, extend(path, fmt.Sprintf("%d", i)))...) } case []interface{}: ba, ok := b[i].([]interface{}) if !ok { diffs = append(diffs, diff{ path: extend(path, fmt.Sprintf("%d", i)), a: aVal, b: b[i], }) } else if len(aVal) != len(ba) { diffs = append(diffs, diff{ path: extend(path, fmt.Sprintf("%d", i)), a: aVal, b: b[i], }) } else { diffs = append(diffs, diffArray(aVal, ba, extend(path, fmt.Sprintf("%d", i)))...) } default: if !reflect.DeepEqual(v, b[i]) { diffs = append(diffs, diff{ path: extend(path, fmt.Sprintf("%d", i)), a: v, b: b[i], }) } } } return diffs } func diffManifestLists(a map[string]manifest, b map[string]manifest) map[string][]diff { diffs := map[string][]diff{} for k, am := range a { bm, ok := b[k] if !ok { diffs[k] = []diff{{ a: am, b: nil, path: []string{}, }} } else { diffs[k] = diffManifest(am, bm, []string{}) } } for k, bm := range b { _, ok := a[k] if !ok { diffs[k] = []diff{{ a: nil, b: bm, path: []string{}, }} } } return diffs } // extend returns a new slice which is a copy of slice with next appended to it. // The advantage of using extend instead of append is that modifying the // returned slice will not modify the original. func extend(slice []string, next string) []string { new := make([]string, len(slice)+1) copy(new, slice) new[len(slice)] = next return new }