github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/kubernetes/manifest/manifest.go (about) 1 package manifest 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "fmt" 7 "text/template" 8 9 "github.com/Masterminds/sprig/v3" 10 "github.com/pkg/errors" 11 "github.com/stretchr/objx" 12 yaml "gopkg.in/yaml.v2" 13 ) 14 15 // Manifest represents a Kubernetes API object. The fields `apiVersion` and 16 // `kind` are required, `metadata.name` should be present as well 17 type Manifest map[string]interface{} 18 19 // New creates a new Manifest 20 func New(raw map[string]interface{}) (Manifest, error) { 21 m := Manifest(raw) 22 if err := m.Verify(); err != nil { 23 return nil, err 24 } 25 return m, nil 26 } 27 28 // NewFromObj creates a new Manifest from an objx.Map 29 func NewFromObj(raw objx.Map) (Manifest, error) { 30 return New(map[string]interface{}(raw)) 31 } 32 33 // String returns the Manifest in yaml representation 34 func (m Manifest) String() string { 35 y, err := yaml.Marshal(m) 36 if err != nil { 37 // this should never go wrong in normal operations 38 panic(errors.Wrap(err, "formatting manifest")) 39 } 40 return string(y) 41 } 42 43 var ( 44 ErrInvalidStr = fmt.Errorf("missing or not of string type") 45 ErrInvalidMap = fmt.Errorf("missing or not an object") 46 ) 47 48 // Verify checks whether the manifest is correctly structured 49 func (m Manifest) Verify() error { 50 o := m2o(m) 51 fields := make(map[string]error) 52 53 if !o.Get("kind").IsStr() { 54 fields["kind"] = ErrInvalidStr 55 } 56 if !o.Get("apiVersion").IsStr() { 57 fields["apiVersion"] = ErrInvalidStr 58 } 59 60 // Lists don't have `metadata` 61 if !m.IsList() { 62 if !o.Get("metadata").IsMSI() { 63 fields["metadata"] = ErrInvalidMap 64 } 65 if !o.Get("metadata.name").IsStr() && !o.Get("metadata.generateName").IsStr() { 66 fields["metadata.name"] = ErrInvalidStr 67 } 68 69 if err := verifyMSS(o.Get("metadata.labels").Data()); err != nil { 70 fields["metadata.labels"] = err 71 } 72 if err := verifyMSS(o.Get("metadata.annotations").Data()); err != nil { 73 fields["metadata.annotations"] = err 74 } 75 } 76 77 if len(fields) == 0 { 78 return nil 79 } 80 81 return &SchemaError{ 82 Fields: fields, 83 Manifest: m, 84 } 85 } 86 87 // verifyMSS checks that ptr is either nil or a string map 88 func verifyMSS(ptr interface{}) error { 89 if ptr == nil { 90 return nil 91 } 92 93 switch t := ptr.(type) { 94 case map[string]string: 95 return nil 96 case map[string]interface{}: 97 for k, v := range t { 98 if _, ok := v.(string); !ok { 99 return fmt.Errorf("contains non-string field '%s' of type '%T'", k, v) 100 } 101 } 102 return nil 103 default: 104 return fmt.Errorf("must be object, but got '%T' instead", ptr) 105 } 106 } 107 108 // IsList returns whether the manifest is a List type, containing other 109 // manifests as children. Code based on 110 // https://github.com/kubernetes/apimachinery/blob/61490fe38e784592212b24b9878306b09be45ab0/pkg/apis/meta/v1/unstructured/unstructured.go#L54 111 func (m Manifest) IsList() bool { 112 items, ok := m["items"] 113 if !ok { 114 return false 115 } 116 _, ok = items.([]interface{}) 117 return ok 118 } 119 120 // Items returns list items if the manifest is of List type 121 func (m Manifest) Items() (List, error) { 122 if !m.IsList() { 123 return nil, fmt.Errorf("attempt to unwrap non-list object '%s' of kind '%s'", m.Metadata().Name(), m.Kind()) 124 } 125 126 // This is safe, IsList() asserts this 127 items := m["items"].([]interface{}) 128 list := make(List, 0, len(items)) 129 for _, i := range items { 130 child, ok := i.(map[string]interface{}) 131 if !ok { 132 return nil, fmt.Errorf("unwrapped list item is not an object, but '%T'", child) 133 } 134 135 m := Manifest(child) 136 list = append(list, m) 137 } 138 139 return list, nil 140 } 141 142 // Kind returns the kind of the API object 143 func (m Manifest) Kind() string { 144 return m["kind"].(string) 145 } 146 147 // KindName returns kind and metadata.name in the `<kind>/<name>` format 148 func (m Manifest) KindName() string { 149 return fmt.Sprintf("%s/%s", 150 m.Kind(), 151 m.Metadata().Name(), 152 ) 153 } 154 155 // APIVersion returns the version of the API this object uses 156 func (m Manifest) APIVersion() string { 157 return m["apiVersion"].(string) 158 } 159 160 // Metadata returns the metadata of this object 161 func (m Manifest) Metadata() Metadata { 162 if m["metadata"] == nil { 163 m["metadata"] = make(map[string]interface{}) 164 } 165 return Metadata(m["metadata"].(map[string]interface{})) 166 } 167 168 // UnmarshalJSON validates the Manifest during json parsing 169 func (m *Manifest) UnmarshalJSON(data []byte) error { 170 type tmp Manifest 171 var t tmp 172 if err := json.Unmarshal(data, &t); err != nil { 173 return err 174 } 175 *m = Manifest(t) 176 return m.Verify() 177 } 178 179 // UnmarshalYAML validates the Manifest during yaml parsing 180 func (m *Manifest) UnmarshalYAML(unmarshal func(interface{}) error) error { 181 var tmp map[string]interface{} 182 if err := unmarshal(&tmp); err != nil { 183 return err 184 } 185 186 data, err := json.Marshal(tmp) 187 if err != nil { 188 return err 189 } 190 191 return json.Unmarshal(data, m) 192 } 193 194 // Metadata is the metadata object from the Manifest 195 type Metadata map[string]interface{} 196 197 // Name of the manifest 198 func (m Metadata) Name() string { 199 name, ok := m["name"] 200 if !ok { 201 return "" 202 } 203 return name.(string) 204 } 205 206 // HasNamespace returns whether the manifest has a namespace set 207 func (m Metadata) HasNamespace() bool { 208 return m2o(m).Get("namespace").IsStr() 209 } 210 211 // Namespace of the manifest 212 func (m Metadata) Namespace() string { 213 if !m.HasNamespace() { 214 return "" 215 } 216 return m["namespace"].(string) 217 } 218 219 func (m Metadata) UID() string { 220 uid, ok := m["uid"].(string) 221 if !ok { 222 return "" 223 } 224 return uid 225 } 226 227 // Labels of the manifest 228 func (m Metadata) Labels() map[string]interface{} { 229 return safeMSI(m, "labels") 230 } 231 232 // Annotations of the manifest 233 func (m Metadata) Annotations() map[string]interface{} { 234 return safeMSI(m, "annotations") 235 } 236 237 // Managed fields of the manifest 238 func (m Metadata) ManagedFields() []interface{} { 239 items, ok := m["managedFields"] 240 if !ok { 241 return make([]interface{}, 0) 242 } 243 list, ok := items.([]interface{}) 244 if !ok { 245 return make([]interface{}, 0) 246 } 247 return list 248 } 249 250 func safeMSI(m map[string]interface{}, key string) map[string]interface{} { 251 switch t := m[key].(type) { 252 case map[string]interface{}: 253 return t 254 default: 255 m[key] = make(map[string]interface{}) 256 return m[key].(map[string]interface{}) 257 } 258 } 259 260 // List of individual Manifests 261 type List []Manifest 262 263 // String returns the List as a yaml stream. In case of an error, it is 264 // returned as a string instead. 265 func (m List) String() string { 266 buf := bytes.Buffer{} 267 enc := yaml.NewEncoder(&buf) 268 269 for _, d := range m { 270 if err := enc.Encode(d); err != nil { 271 // This should never happen in normal operations 272 panic(errors.Wrap(err, "formatting manifests")) 273 } 274 } 275 276 return buf.String() 277 } 278 279 func (m List) Namespaces() []string { 280 namespaces := map[string]struct{}{} 281 for _, manifest := range m { 282 if namespace := manifest.Metadata().Namespace(); namespace != "" { 283 namespaces[namespace] = struct{}{} 284 } 285 } 286 keys := []string{} 287 for k := range namespaces { 288 keys = append(keys, k) 289 } 290 return keys 291 } 292 293 func m2o(m interface{}) objx.Map { 294 switch mm := m.(type) { 295 case Metadata: 296 return objx.New(map[string]interface{}(mm)) 297 case Manifest: 298 return objx.New(map[string]interface{}(mm)) 299 } 300 return nil 301 } 302 303 // DefaultNameFormat to use when no nameFormat is supplied 304 const DefaultNameFormat = `{{ print .kind "_" .metadata.name | snakecase }}` 305 306 func ListAsMap(list List, nameFormat string) (map[string]interface{}, error) { 307 if nameFormat == "" { 308 nameFormat = DefaultNameFormat 309 } 310 311 tmpl, err := template.New(""). 312 Funcs(sprig.TxtFuncMap()). 313 Parse(nameFormat) 314 if err != nil { 315 return nil, fmt.Errorf("parsing name format: %w", err) 316 } 317 318 out := make(map[string]interface{}) 319 for _, m := range list { 320 var buf bytes.Buffer 321 if err := tmpl.Execute(&buf, m); err != nil { 322 return nil, err 323 } 324 name := buf.String() 325 326 if _, ok := out[name]; ok { 327 return nil, ErrorDuplicateName{name: name, format: nameFormat} 328 } 329 out[name] = map[string]interface{}(m) 330 } 331 332 return out, nil 333 }