package manifest
import (
"bufio"
"bytes"
"fmt"
"io"
"log"
"strings"
jsoniter "github.com/json-iterator/go"
"gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/runtime"
)
const (
hookAnnotation = "helm.sh/hook"
resourcePolicyAnnotation = "helm.sh/resource-policy"
)
var yamlSeparator = []byte("\n---\n")
type MappingResult struct {
Name string
Kind string
Content string
ResourcePolicy string
}
type metadata struct {
APIVersion string `yaml:"apiVersion"`
Kind string
Metadata struct {
Namespace string
Name string
Annotations map[string]string
}
}
func (m metadata) String() string {
apiBase := m.APIVersion
sp := strings.Split(apiBase, "/")
if len(sp) > 1 {
apiBase = strings.Join(sp[:len(sp)-1], "/")
}
name := m.Metadata.Name
if a := m.Metadata.Annotations; a != nil {
if baseName, ok := a["helm-diff/base-name"]; ok {
name = baseName
}
}
return fmt.Sprintf("%s, %s, %s (%s)", m.Metadata.Namespace, name, m.Kind, apiBase)
}
func scanYamlSpecs(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.Index(data, yamlSeparator); i >= 0 {
return i + len(yamlSeparator), data[0:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
func Parse(manifest []byte, defaultNamespace string, normalizeManifests bool, excludedHooks ...string) map[string]*MappingResult {
scanner := bufio.NewScanner(io.MultiReader(strings.NewReader("\n"), bytes.NewReader(manifest)))
scanner.Split(scanYamlSpecs)
scanner.Buffer(make([]byte, bufio.MaxScanTokenSize), 10485760)
result := make(map[string]*MappingResult)
for scanner.Scan() {
content := bytes.TrimSpace(scanner.Bytes())
if len(content) == 0 {
continue
}
parsed, err := parseContent(content, defaultNamespace, normalizeManifests, excludedHooks...)
if err != nil {
log.Fatalf("%v", err)
}
for _, p := range parsed {
name := p.Name
if _, ok := result[name]; ok {
log.Printf("Error: Found duplicate key %#v in manifest", name)
} else {
result[name] = p
}
}
}
if err := scanner.Err(); err != nil {
log.Fatalf("Error reading input: %s", err)
}
return result
}
func ParseObject(object runtime.Object, defaultNamespace string, excludedHooks ...string) (*MappingResult, string, error) {
json, _ := jsoniter.ConfigCompatibleWithStandardLibrary.Marshal(object)
var objectMap map[string]interface{}
err := jsoniter.Unmarshal(json, &objectMap)
if err != nil {
return nil, "", fmt.Errorf("could not unmarshal byte sequence: %w", err)
}
metadata := objectMap["metadata"].(map[string]interface{})
var oldRelease string
if a := metadata["annotations"]; a != nil {
annotations := a.(map[string]interface{})
if releaseNs, ok := annotations["meta.helm.sh/release-namespace"].(string); ok {
oldRelease += releaseNs + "/"
}
if releaseName, ok := annotations["meta.helm.sh/release-name"].(string); ok {
oldRelease += releaseName
}
}
purgedObj, _ := deleteStatusAndTidyMetadata(json)
content, err := yaml.Marshal(purgedObj)
if err != nil {
return nil, "", err
}
result, err := parseContent(content, defaultNamespace, true, excludedHooks...)
if err != nil {
return nil, "", err
}
if len(result) != 1 {
return nil, "", fmt.Errorf("failed to parse content of Kubernetes resource %s", metadata["name"])
}
result[0].Content = strings.TrimSuffix(result[0].Content, "\n")
return result[0], oldRelease, nil
}
func parseContent(content []byte, defaultNamespace string, normalizeManifests bool, excludedHooks ...string) ([]*MappingResult, error) {
var parsedMetadata metadata
if err := yaml.Unmarshal(content, &parsedMetadata); err != nil {
log.Fatalf("YAML unmarshal error: %s\nCan't unmarshal %s", err, content)
}
if parsedMetadata.APIVersion == "" && parsedMetadata.Kind == "" {
return nil, nil
}
if strings.HasSuffix(parsedMetadata.Kind, "List") {
type ListV1 struct {
Items []yaml.MapSlice `yaml:"items"`
}
var list ListV1
if err := yaml.Unmarshal(content, &list); err != nil {
log.Fatalf("YAML unmarshal error: %s\nCan't unmarshal %s", err, content)
}
var result []*MappingResult
for _, item := range list.Items {
subcontent, err := yaml.Marshal(item)
if err != nil {
log.Printf("YAML marshal error: %s\nCan't marshal %v", err, item)
}
subs, err := parseContent(subcontent, defaultNamespace, normalizeManifests, excludedHooks...)
if err != nil {
return nil, fmt.Errorf("Parsing YAML list item: %w", err)
}
result = append(result, subs...)
}
return result, nil
}
if normalizeManifests {
var normalizeErr error
content, normalizeErr = normalizeContent(content)
if normalizeErr != nil {
log.Fatalf("Error normalizing manifests: %v", normalizeErr)
}
}
if isHook(parsedMetadata, excludedHooks...) {
return nil, nil
}
if parsedMetadata.Metadata.Namespace == "" {
parsedMetadata.Metadata.Namespace = defaultNamespace
}
name := parsedMetadata.String()
return []*MappingResult{
{
Name: name,
Kind: parsedMetadata.Kind,
Content: string(content),
ResourcePolicy: parsedMetadata.Metadata.Annotations[resourcePolicyAnnotation],
},
}, nil
}
func normalizeContent(content []byte) ([]byte, error) {
var object map[interface{}]interface{}
if err := yaml.Unmarshal(content, &object); err != nil {
return nil, err
}
normalizedContent, err := yaml.Marshal(object)
if err != nil {
return nil, err
}
return normalizedContent, nil
}
func isHook(metadata metadata, hooks ...string) bool {
for _, hook := range hooks {
if metadata.Metadata.Annotations[hookAnnotation] == hook {
return true
}
}
return false
}